spb/vquant Public MIT
VibeQuant — AI-powered institutional-grade financial intelligence platform.
TypeScript 84.3%
Python 11.7%
JavaScript 1.6%
CSS 1.5%
HTML 0.7%
1/*2 * =============================================================================3 * VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 * File: server/services/pdfService.ts6 *7 * Author: Simon-Pierre Boucher8 * Contact: contact@spboucher.ai9 * Website: https://www.spboucher.ai10 * Demo: https://www.vquant.ai11 * License: MIT (see LICENSE)12 *13 * Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import { exec } from 'child_process';18import { promisify } from 'util';19import fs from 'fs';20import path from 'path';21import puppeteer from 'puppeteer';22import { marked } from 'marked';2324const execAsync = promisify(exec);2526// VQuant SVG logo as data URI for PDF embedding27const VIBEQUANT_LOGO_SVG = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="100%" height="100%" preserveAspectRatio="xMidYMid meet"><defs><linearGradient id="Gradient1" gradientUnits="userSpaceOnUse" x1="1470.71" y1="673.635" x2="1501.29" y2="700.365"><stop offset="0" stop-opacity="1" stop-color="rgb(17,134,140)"/><stop offset="1" stop-opacity="1" stop-color="rgb(28,158,154)"/></linearGradient></defs><path fill="rgb(17,153,147)" d="M 993.756 1055.25 C 944.353 958.827 938.872 851.299 978.887 750.14 C 1029.14 623.09 1155.55 532.449 1294.04 534.757 C 1294.07 572.84 1295.12 617.291 1293.98 654.882 C 1262.91 651.603 1224.36 663.345 1196.77 677.525 C 1134.79 709.603 1090.4 767.711 1075.75 835.943 C 1073.21 848.239 1071.94 858.374 1070.84 870.657 C 1070.17 878.115 1071.43 898.64 1069.28 904.273 C 1066.56 911.402 1058.93 923.613 1055.3 931.403 C 1035.43 973.004 1014.92 1014.29 993.756 1055.25z"/><path fill="rgb(239,99,43)" d="M 669.285 770.587 C 701.479 833.319 732.93 900.533 764.094 964.066 C 774.722 985.832 785.481 1007.53 796.369 1029.17 C 801.268 1038.96 812.213 1060.2 815.792 1069.63 L 817.931 1072.15 L 818.591 1072.3 C 820.802 1078.42 824.273 1084.92 827.271 1090.74 C 843.263 1121.8 858.788 1153.14 874.283 1184.46 C 882.766 1201.6 894.22 1221.68 900.487 1239.45 C 889.586 1238.26 858.969 1239.07 846.206 1239.05 L 734.501 1238.88 C 729.825 1232.62 720.391 1213.62 716.737 1206.4 L 689.065 1151.46 L 581.888 937.187 L 580.461 934.083 C 608.843 879.419 640.102 824.968 669.285 770.587z"/><path fill="rgb(244,148,34)" d="M 669.285 770.587 C 701.479 833.319 732.93 900.533 764.094 964.066 C 774.722 985.832 785.481 1007.53 796.369 1029.17 C 801.268 1038.96 812.213 1060.2 815.792 1069.63 C 808.029 1067.58 759.78 1038.78 748.965 1032.52 C 708.83 1009.29 667.976 985.878 627.721 962.72 C 617.603 956.9 591.585 939.428 581.888 937.187 L 580.461 934.083 C 608.843 879.419 640.102 824.968 669.285 770.587z"/><path fill="rgb(101,46,137)" d="M 817.931 1072.15 L 818.591 1072.3 C 820.802 1078.42 824.273 1084.92 827.271 1090.74 C 843.263 1121.8 858.788 1153.14 874.283 1184.46 C 882.766 1201.6 894.22 1221.68 900.487 1239.45 C 889.586 1238.26 858.969 1239.07 846.206 1239.05 L 734.501 1238.88 C 744.908 1219.3 755.504 1196.58 765.582 1176.42 L 817.931 1072.15z"/><path fill="rgb(17,153,147)" d="M 1298.28 1001.63 C 1343.27 1003.19 1397.4 999.467 1440.69 1002.35 C 1464.39 1025.31 1494.92 1062.4 1517.87 1087.95 L 1615.57 1197.71 C 1627.84 1211.57 1640.63 1224.87 1652.74 1238.97 C 1622.28 1238.58 1591.82 1238.36 1561.35 1238.3 C 1546.09 1238.25 1524.62 1238.81 1509.79 1237.88 C 1499.67 1231.24 1476.25 1200.92 1466.18 1190.58 C 1449.52 1196.27 1433.3 1207.95 1417.14 1214.26 C 1380.6 1228.53 1332.86 1238.17 1293.76 1238.09 C 1294.06 1200.9 1292.45 1155.26 1294.22 1119.05 C 1325.65 1119.34 1356.72 1112.41 1385.04 1098.79 C 1377.9 1089.31 1369.62 1080.71 1361.46 1072.11 C 1339.91 1049.4 1320.09 1024 1298.28 1001.63z"/><path fill="rgb(43,159,212)" d="M 393.721 559.919 C 418.527 557.964 450.79 559.839 476.534 559.254 C 503.261 558.646 531.945 560.009 558.901 559.593 C 562.001 559.545 563.57 559.971 565.667 561.757 C 599.747 631.557 635.539 700.802 669.285 770.587 C 640.102 824.968 608.843 879.419 580.461 934.083 L 576.246 925.511 C 560.019 895.03 542.654 857.155 526.868 825.475 L 436.539 645.994 C 422.817 618.526 408.303 586.358 393.721 559.919z"/><path fill="rgb(32,114,178)" d="M 565.667 561.757 C 599.747 631.557 635.539 700.802 669.285 770.587 C 640.102 824.968 608.843 879.419 580.461 934.083 L 576.246 925.511 C 577.765 924.241 579.352 923.267 579.279 920.981 C 578.379 892.553 577.378 863.397 576.103 835.036 L 567.347 628.346 C 566.855 616.183 563.838 570.741 565.667 561.757z"/><path fill="rgb(17,153,147)" d="M 1541.12 645.162 C 1551.25 649.427 1572.39 682.795 1578.98 692.511 C 1653.35 802.089 1653.77 955.749 1586.53 1068.72 C 1581.38 1077.38 1572.16 1094.1 1565.29 1100.87 L 1544.17 1077.67 C 1530.55 1061.3 1515.23 1045.5 1501.49 1029.15 C 1495.93 1022.54 1488.6 1014.93 1483.85 1007.95 C 1497.88 983.5 1508.94 958.33 1514.22 930.439 C 1524.31 877.143 1515.78 822 1490.06 774.243 C 1481.31 758.28 1472.67 747.185 1462.57 732.445 C 1475.3 732.666 1528.81 735.26 1536.46 728.864 C 1538.74 726.958 1539.98 724.061 1540.47 721.182 C 1541.8 713.296 1540.55 703.741 1540.44 695.673 C 1540.19 678.883 1539.92 661.92 1541.12 645.162z"/><path fill="rgb(15,106,131)" d="M 1078.31 945.781 C 1090.08 990.644 1109.36 1024.52 1142.48 1056.98 C 1181.32 1095.05 1239.41 1120.13 1294.22 1119.05 C 1292.45 1155.26 1294.06 1200.9 1293.76 1238.09 C 1213.49 1236.97 1141.28 1212.68 1079.84 1160.39 C 1055.43 1139.62 1027.61 1109.8 1010.36 1082.73 C 1026.59 1046.46 1046.84 1011.08 1063.55 974.93 C 1068 965.319 1072.97 954.837 1078.31 945.781z"/><path fill="rgb(17,153,147)" d="M 947.373 1369.22 C 975.161 1367.44 994.172 1369.89 1018.22 1384.62 C 1069.66 1416.15 1072.78 1490.94 1027.53 1529.15 C 1019.64 1535.81 1004.56 1544.49 994.541 1546.57 C 1007.38 1557.38 1022.88 1565.62 1038.54 1555.02 C 1042.16 1552.57 1045.85 1550.2 1050.17 1549.44 C 1055.2 1550.02 1066.73 1564.18 1066.96 1569.28 C 1064.02 1574.94 1058.75 1578.18 1053.57 1581.83 C 1010.13 1604.69 977.021 1582.13 946.701 1551.6 C 929.917 1547.82 921.015 1546.55 906.246 1537.08 C 886.265 1524.25 871.053 1502.93 866.473 1479.64 C 859.202 1442.67 871.102 1407.38 902.795 1385.83 C 917.626 1375.74 929.832 1372.5 947.373 1369.22z M 970.6 1516.69 C 1001.93 1511.53 1023.14 1481.95 1017.99 1450.62 C 1012.83 1419.3 983.254 1398.08 951.924 1403.23 C 920.592 1408.39 899.372 1437.97 904.53 1469.3 C 909.687 1500.63 939.268 1521.85 970.6 1516.69z"/><path fill="rgb(159,56,144)" d="M 818.591 1072.3 C 841.738 1024.57 865.347 977.062 889.415 929.788 C 900.162 908.412 917.171 876.32 926.007 854.871 C 925.102 926.705 926.979 971.12 956.908 1038.88 C 963.494 1053.8 972.139 1070.07 978.453 1084.26 C 972.023 1098.18 903.083 1237.22 900.487 1239.45 C 894.22 1221.68 882.766 1201.6 874.283 1184.46 C 858.788 1153.14 843.263 1121.8 827.271 1090.74 C 824.273 1084.92 820.802 1078.42 818.591 1072.3z"/><path fill="rgb(32,114,178)" d="M 505.083 1371.14 C 538.236 1372.4 571.723 1370.02 604.824 1371.44 C 620.129 1372.09 643.393 1379.12 653.218 1391.27 C 664.585 1405.32 665.838 1430.88 653.578 1444.91 C 649.991 1449.07 645.968 1452.14 641.689 1455.46 C 668.149 1470.85 676.8 1492.53 666.454 1521.66 C 664.026 1525.47 661.078 1529.6 657.858 1532.76 C 639.112 1551.21 600.94 1548.3 577.425 1548.29 L 505.503 1548.17 C 503.759 1492.14 505.143 1427.85 505.083 1371.14z M 545.117 1517.81 C 571.122 1515.19 601.086 1522.62 623.02 1510.45 C 631.301 1501.46 631.356 1487.47 622.227 1480.65 C 605.339 1468.03 565.862 1473.86 545.332 1473.18 C 545.26 1488 545.475 1503.02 545.117 1517.81z M 545.016 1402.93 C 545.01 1416.16 545.204 1429.9 544.97 1443.08 C 555.804 1443.07 599.462 1444.24 607.677 1441.2 C 616.838 1436.47 617.976 1435.55 621.671 1425.8 C 619.503 1400.67 602.805 1402.54 582.18 1402.35 C 574.269 1402.27 566.23 1402.36 558.311 1402.47 L 545.016 1402.93z"/><path fill="rgb(108,188,90)" d="M 1294.04 534.757 C 1380.88 536.165 1453.33 562.688 1518.32 620.589 C 1520.85 622.843 1520.97 623.846 1521.48 626.946 C 1520.26 629.147 1520.4 630.146 1517.73 630.444 C 1501.85 632.21 1483.97 630.986 1467.91 631.262 C 1458.05 631.431 1448.71 631.963 1439.39 631.92 C 1436.27 634.92 1434.45 636.605 1431.8 639.976 C 1431.92 661.559 1431.71 683.617 1432.03 705.141 C 1388.09 668.966 1351.25 654.932 1293.98 654.882 C 1295.12 617.291 1294.07 572.84 1294.04 534.757z"/><path fill="rgb(17,153,147)" d="M 1489.77 1371.1 C 1501.1 1371.07 1512.58 1370.88 1523.88 1371.54 L 1615.16 1480.68 C 1614.47 1445 1614.9 1407.68 1614.79 1371.86 C 1628.55 1371.99 1642.32 1371.99 1656.08 1371.86 L 1655.96 1548.99 L 1621.69 1548.77 C 1612.79 1539.5 1600.98 1524.41 1592.45 1514.2 L 1530.04 1439.44 L 1530.08 1548.99 C 1516.88 1548.61 1502.85 1548.93 1489.58 1549.04 C 1490.42 1528.77 1489.7 1503.16 1489.69 1482.44 L 1489.77 1371.1z"/><path fill="rgb(17,153,147)" d="M 1345.95 1371.72 C 1360.03 1371.58 1374.11 1371.68 1388.19 1372 C 1400.61 1396.33 1412.65 1424.36 1424.25 1449.37 C 1439.06 1481.29 1456.65 1516.06 1469.63 1548.44 C 1455.4 1548.3 1440.58 1548.69 1426.31 1548.87 C 1420.94 1536.97 1416.17 1523.31 1411.14 1511.03 L 1392.52 1510.79 C 1369.68 1510.62 1346.85 1510.68 1324.02 1510.97 C 1319.85 1524.24 1314.77 1536.36 1308.95 1548.95 C 1295.54 1549.87 1280.82 1549.42 1267.25 1549.28 C 1273.15 1532.26 1283.07 1512.77 1290.29 1496.02 C 1307.87 1455.22 1329.92 1412.79 1345.95 1371.72z M 1339.02 1479.32 L 1370.5 1479.22 L 1396.4 1479.27 C 1392.44 1470.37 1370.3 1415.05 1366.72 1411.64 C 1358.71 1433.17 1345.74 1459.32 1339.02 1479.32z"/><path fill="rgb(244,148,34)" d="M 822.357 475.859 C 830.346 483.664 819.834 673.808 825.945 700.727 C 825.739 712.378 816.931 760.768 814.584 774.381 C 808.291 811.178 802.335 848.033 796.719 884.94 C 791.64 867.739 785.184 849.674 779.543 832.505 L 737.935 708.202 C 734.896 699.176 722.805 657.199 719.307 650.774 C 718.445 639.521 717.953 551.029 720.176 545.218 C 740.64 597.793 759.092 655.018 778.174 708.585 C 780.408 695.292 783.046 682.082 785.598 668.844 C 792.733 631.847 799.309 594.73 806.797 557.802 C 812.317 530.58 815.818 502.856 822.357 475.859z"/><path fill="rgb(32,114,178)" d="M 702.021 1371.2 L 772.334 1371.22 C 794.699 1371.45 816.462 1372.31 838.943 1371.43 L 838.888 1404.3 C 806.994 1404.89 774.957 1404.25 742.945 1404.79 C 742.94 1417.55 742.929 1429.72 743.496 1442.48 C 771.642 1442.61 799.79 1442.54 827.935 1442.27 C 827.783 1453.01 827.526 1463.75 827.891 1474.48 C 804.677 1475.02 781.07 1474.6 757.75 1474.94 L 742.976 1474.92 C 743.022 1488.63 743.178 1502.34 743.445 1516.05 C 776.263 1515.35 809.211 1516.33 842.073 1515.88 C 842.047 1526.88 842.111 1537.87 842.267 1548.87 C 826.587 1548.01 806.424 1548.59 790.351 1548.59 C 761.181 1548.48 732.01 1548.63 702.843 1549.05 C 701.311 1490.5 702.252 1429.93 702.021 1371.2z"/><path fill="rgb(17,153,147)" d="M 1090.18 1371.32 C 1100.19 1372.07 1121.25 1371.8 1131.3 1371.08 L 1131.4 1443.26 C 1131.42 1474.91 1126.29 1514.45 1169.83 1516.35 C 1218.69 1518.49 1213.32 1474.93 1213.16 1441.68 L 1212.98 1371.64 C 1226.05 1372.15 1239.8 1371.93 1252.94 1371.91 L 1252.69 1406.5 L 1252.9 1452.27 C 1253.04 1479.41 1254.21 1508.87 1233.79 1529.56 C 1194.42 1569.45 1105.4 1556.06 1092.82 1496.21 C 1088.8 1477.07 1090.07 1455.26 1090.13 1435.79 C 1090.26 1414.3 1090.28 1392.81 1090.18 1371.32z"/><path fill="rgb(32,114,178)" d="M 403.024 1371.73 L 403.248 1371.83 C 403.953 1374.2 334.722 1531.49 327.948 1548.71 C 314.574 1548.67 301.2 1548.72 287.826 1548.84 C 284.548 1542.35 281.085 1534.7 278.296 1528 C 261.806 1488.35 243.225 1449.15 227.09 1409.41 C 222.114 1398.18 215.517 1383.72 211.81 1372.32 C 220.9 1371.61 244.848 1370.43 252.719 1373.11 C 260.573 1381.32 300.101 1478.96 308.576 1497.38 C 328.16 1461.07 343.893 1410.77 364.266 1371.82 C 376.955 1371.56 390.283 1371.75 403.024 1371.73z"/><path fill="rgb(254,194,29)" d="M 1359.08 816.906 C 1384.41 817.218 1410.24 816.96 1435.61 816.943 C 1433.67 868.566 1437.04 923.309 1435.2 974.448 L 1432.75 975.53 L 1360.11 975.484 C 1358.43 924.508 1359.12 868.221 1359.08 816.906z"/><path fill="rgb(17,153,147)" d="M 1697.3 1371.53 C 1710.89 1370.6 1732.46 1371.14 1746.65 1371.26 C 1776.72 1371.81 1806.8 1371.89 1836.87 1371.49 L 1836.96 1404.92 C 1820.37 1404.06 1794.91 1404.5 1778.41 1405.07 C 1780.03 1437.1 1778.97 1476.58 1779 1509.25 L 1778.98 1548.95 C 1766.74 1548 1750.46 1548.73 1738.02 1549.06 L 1738.02 1405 C 1722.4 1404.02 1694.98 1404.21 1679.14 1404.76 C 1679.31 1393.73 1679.27 1382.69 1679 1371.66 C 1685.02 1371.95 1691.25 1371.68 1697.3 1371.53z"/><path fill="rgb(159,56,144)" d="M 825.343 457.863 C 839.616 517.848 858.268 578.968 873.273 638.933 C 878.887 661.366 892.127 702.442 895.251 724.137 C 894.229 731.435 894.063 738.639 893.946 745.997 C 893.503 773.884 891.712 801.714 890.924 829.587 C 890.776 834.802 891.215 839.151 888.047 843.533 L 886.296 843.965 C 868.691 777.861 853.616 710.03 836.03 643.382 C 834.125 659.622 829.041 684.608 825.945 700.727 C 819.834 673.808 830.346 483.664 822.357 475.859 C 823.025 469.426 824.091 464.196 825.343 457.863z"/><path fill="rgb(101,46,137)" d="M 895.251 724.137 C 896.065 724.64 895.468 724.353 897.216 724.419 C 900.728 718.941 912.649 687.364 915.995 679.037 C 927.918 649.454 940.053 619.958 952.401 590.55 C 954.357 585.785 961.323 566.925 963.846 564.149 C 966.178 565.858 965.67 566.577 966.673 570.123 C 973.354 587.374 979.839 604.701 986.124 622.1 C 990.046 632.796 994.915 647.228 999.898 657.136 C 990.234 669.188 971.735 698.78 964.861 712.735 C 964.916 696.796 964.163 680.798 963.869 664.845 C 946.787 703.939 932.372 745.931 916.778 785.959 C 911.845 798.734 906.827 811.475 901.724 824.183 C 897.345 834.913 891.879 846.288 889.249 857.407 C 888.197 852.97 887.03 848.456 886.296 843.965 L 888.047 843.533 C 891.215 839.151 890.776 834.802 890.924 829.587 C 891.712 801.714 893.503 773.884 893.946 745.997 C 894.063 738.639 894.229 731.435 895.251 724.137z"/><path fill="rgb(159,56,144)" d="M 966.673 570.123 C 973.354 587.374 979.839 604.701 986.124 622.1 C 990.046 632.796 994.915 647.228 999.898 657.136 C 990.234 669.188 971.735 698.78 964.861 712.735 C 964.916 696.796 964.163 680.798 963.869 664.845 L 963.663 607.094 C 963.636 597.448 962.257 578.008 966.673 570.123z"/><path fill="rgb(108,188,90)" d="M 1246.3 861.104 C 1271.35 861.442 1296.41 861.476 1321.47 861.205 C 1322.02 872.843 1323.11 971.784 1320.74 975.009 C 1301.64 974.706 1264.83 974.111 1246.6 975.719 C 1245.53 939.169 1248.29 896.29 1246.3 861.104z"/><path fill="rgb(17,153,147)" d="M 1207.18 934.681 C 1207.55 935.113 1207.92 935.546 1208.29 935.978 C 1208.06 971.762 1207.84 1037.14 1208.27 1069.94 C 1193.68 1062.94 1176.3 1050.8 1164.34 1039.96 C 1152.54 1028.04 1143.81 1018.66 1133.65 1005.05 C 1133.96 983.659 1134.24 955.962 1133.16 934.862 C 1157.83 935.006 1182.51 934.945 1207.18 934.681z"/><path fill="rgb(32,114,178)" d="M 676.639 666.719 C 680.757 655.445 717.095 545.04 719.459 542.856 L 720.176 545.218 C 717.953 551.029 718.445 639.521 719.307 650.774 C 718.543 664.626 718.914 683.365 718.768 697.596 C 718.534 735.536 718.651 773.477 719.12 811.415 C 697.619 763.98 670.566 713.811 648.123 666.174 C 657.749 666.193 667.011 666.158 676.639 666.719z"/><path fill="rgb(32,114,178)" d="M 422.866 1371.65 C 433.696 1370.73 451.437 1371.18 462.787 1371.16 C 462.795 1429.92 463.367 1488.74 462.984 1547.48 C 462.587 1547.92 462.191 1548.36 461.794 1548.81 C 451.011 1548.07 433.891 1548.72 422.644 1548.84 C 420.76 1495.8 423.598 1440.73 422.215 1387.52 C 422.114 1383.62 421.862 1375.23 422.866 1371.65z"/><path fill="rgb(108,188,90)" d="M 1246.95 981.749 C 1249.93 986.554 1263.94 1002.19 1268.64 1007.68 C 1285.77 1027.98 1303.23 1048 1321.02 1067.74 C 1321.31 1073.82 1322.92 1086.27 1319.75 1090.44 C 1295.19 1090.85 1270.5 1091.01 1246.7 1084.74 C 1247.54 1055.86 1244.06 1008.36 1246.95 981.749z"/><path fill="rgb(108,188,90)" d="M 1245.45 785.458 C 1243.76 777.315 1243.69 770.795 1248.18 763.552 C 1252.35 756.966 1259 752.344 1266.63 750.737 C 1289.89 745.82 1293.43 767.118 1295.62 783.717 C 1293.67 789.245 1291.18 792.536 1287.42 797.071 C 1275.99 803.655 1268.66 804.035 1256.15 799.127 C 1250.34 795.296 1248.55 791.418 1245.45 785.458z"/><path fill="rgb(17,153,147)" d="M 1195.63 873.272 C 1190.31 882.688 1178.84 892.23 1168 889.383 C 1132.83 880.143 1138.77 827.165 1184.22 840.521 C 1188.86 843.142 1194.18 848.561 1195.5 853.8 C 1196.44 861.366 1196.38 865.826 1195.63 873.272z"/><path fill="rgb(254,194,29)" d="M 1374.96 761.166 C 1373.33 752.244 1372.17 742.045 1377.7 734.277 C 1389.71 717.447 1422 722.788 1424.45 744.784 C 1425.24 751.876 1423.85 761.96 1419.14 767.524 C 1411.32 776.757 1397.19 774.24 1386.84 772.884 C 1381.24 769.488 1378.75 766.199 1374.96 761.166z"/><path fill="rgb(15,106,131)" d="M 1327.55 806.293 C 1343.25 791.146 1359.06 776.103 1374.96 761.166 C 1378.75 766.199 1381.24 769.488 1386.84 772.884 C 1372.32 787.42 1344.44 816.9 1329.93 828.804 C 1316.68 819.204 1299.6 807.495 1287.42 797.071 C 1291.18 792.536 1293.67 789.245 1295.62 783.717 C 1306.59 789.685 1316.51 799.524 1327.55 806.293z"/><path fill="rgb(15,106,131)" d="M 1245.45 785.458 C 1248.55 791.418 1250.34 795.296 1256.15 799.127 C 1244.18 811.771 1228.67 823.882 1215.79 835.755 C 1211.01 840.164 1198.42 849.191 1195.5 853.8 C 1194.18 848.561 1188.86 843.142 1184.22 840.521 C 1187.08 837.104 1199.52 826.492 1203.44 823 L 1245.45 785.458z"/><path fill="url(#Gradient1)" d="M 1478.21 660.962 L 1494.23 661.142 C 1494.63 678.564 1493.61 695.923 1494.54 713.25 C 1489.43 713.167 1483.64 713.489 1478.48 713.656 C 1478.14 696.093 1478.05 678.527 1478.21 660.962z"/><path fill="rgb(15,106,131)" d="M 1504.68 680.904 L 1521.05 681.092 L 1521.03 714.066 C 1515.81 714.132 1510.26 714.008 1505.01 713.975 C 1504.79 702.953 1504.68 691.928 1504.68 680.904z"/><path fill="rgb(15,106,131)" d="M 1450.56 681.07 C 1455.34 681.583 1462.16 681.471 1467.12 681.537 C 1467.09 691.222 1466.71 703.539 1467.61 712.972 L 1449.9 713.254 C 1450.26 702.53 1450.48 691.801 1450.56 681.07z"/><path fill="rgb(254,194,29)" d="M 1511.1 643.928 C 1525.38 643.585 1524.35 656.223 1515.52 663.367 C 1501.11 661.854 1499.16 650.158 1511.1 643.928z"/></svg>`;2829// Convert SVG to base64 data URI30const LOGO_DATA_URI = `data:image/svg+xml;base64,${Buffer.from(VIBEQUANT_LOGO_SVG).toString('base64')}`;3132interface MarkdownPDFOptions {33 question: string;34 answer: string;35}3637/**38 * Convert image paths to base64 data URLs for embedding in PDF39 */40function convertImagesToBase64(markdown: string): string {41 // Find all image references like 42 const imageRegex = /!\[([^\]]*)\]\(\/figures\/([^)]+)\)/g;4344 let result = markdown;45 let match;4647 while ((match = imageRegex.exec(markdown)) !== null) {48 const altText = match[1];49 const filename = match[2];50 const fullMatch = match[0];5152 // Try to read the image file53 const imagePath = path.join(process.cwd(), 'dist', 'public', 'figures', filename);5455 try {56 if (fs.existsSync(imagePath)) {57 // Read image and convert to base6458 const imageBuffer = fs.readFileSync(imagePath);59 const base64Image = imageBuffer.toString('base64');60 const mimeType = 'image/png'; // All our figures are PNG6162 // Replace with base64 data URL63 const dataUrl = `data:${mimeType};base64,${base64Image}`;64 const replacement = ``;6566 result = result.replace(fullMatch, replacement);67 console.log(`✓ Converted image to base64: ${filename}`);68 } else {69 console.warn(`⚠ Image not found: ${imagePath}`);70 }71 } catch (error) {72 console.error(`✗ Error reading image ${filename}:`, error.message);73 }74 }7576 return result;77}7879/**80 * Convert markdown to HTML with styling81 */82function markdownToHTML(markdown: string, title: string): string {83 // Convert images to base64 first84 const markdownWithBase64Images = convertImagesToBase64(markdown);8586 // Protect LaTeX math from markdown parser:87 // $$...$$ (display) and $...$ (inline) → placeholders, then restore after88 let protected_ = markdownWithBase64Images;89 const mathBlocks: string[] = [];9091 // Display math $$...$$92 protected_ = protected_.replace(/\$\$([\s\S]+?)\$\$/g, (_match, expr) => {93 const idx = mathBlocks.length;94 mathBlocks.push(expr);95 return `%%DISPLAYMATH${idx}%%`;96 });9798 // Inline math $...$ (not $$)99 protected_ = protected_.replace(/\$([^\$\n]+?)\$/g, (_match, expr) => {100 const idx = mathBlocks.length;101 mathBlocks.push(expr);102 return `%%INLINEMATH${idx}%%`;103 });104105 let htmlBody = marked.parse(protected_) as string;106107 // Restore math with KaTeX spans108 htmlBody = htmlBody.replace(/%%DISPLAYMATH(\d+)%%/g, (_m, idx) => {109 return `<span class="math-display">\\[${mathBlocks[parseInt(idx)]}\\]</span>`;110 });111 htmlBody = htmlBody.replace(/%%INLINEMATH(\d+)%%/g, (_m, idx) => {112 return `<span class="math-inline">\\(${mathBlocks[parseInt(idx)]}\\)</span>`;113 });114115 return `116<!DOCTYPE html>117<html lang="fr">118<head>119 <meta charset="UTF-8">120 <title>${title}</title>121 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">122 <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>123 <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"124 onload="renderMathInElement(document.body, {delimiters:[{left:'\\\\[',right:'\\\\]',display:true},{left:'\\\\(',right:'\\\\)',display:false}],throwOnError:false});"></script>125 <style>126 @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');127128 * {129 margin: 0;130 padding: 0;131 box-sizing: border-box;132 }133134 body {135 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;136 line-height: 1.6;137 color: #1f2937;138 padding: 40px;139 max-width: 800px;140 margin: 0 auto;141 }142143 h1, h2, h3, h4, h5, h6 {144 color: #111827;145 margin-top: 1.5em;146 margin-bottom: 0.5em;147 font-weight: 600;148 }149150 h1 {151 font-size: 2em;152 color: #6366f1;153 border-bottom: 3px solid #6366f1;154 padding-bottom: 0.3em;155 }156157 h2 {158 font-size: 1.5em;159 color: #4f46e5;160 }161162 h3 {163 font-size: 1.25em;164 }165166 p {167 margin-bottom: 1em;168 }169170 ul, ol {171 margin-left: 2em;172 margin-bottom: 1em;173 }174175 code {176 background: #f3f4f6;177 padding: 0.2em 0.4em;178 border-radius: 3px;179 font-family: 'Monaco', 'Courier New', monospace;180 font-size: 0.9em;181 }182183 pre {184 background: #1f2937;185 color: #f9fafb;186 padding: 1em;187 border-radius: 6px;188 overflow-x: auto;189 margin-bottom: 1em;190 }191192 pre code {193 background: none;194 color: #f9fafb;195 padding: 0;196 }197198 table {199 width: 100%;200 border-collapse: collapse;201 margin-bottom: 1em;202 }203204 th, td {205 border: 1px solid #e5e7eb;206 padding: 0.75em;207 text-align: left;208 }209210 th {211 background: #f3f4f6;212 font-weight: 600;213 }214215 img {216 max-width: 100%;217 height: auto;218 display: block;219 margin: 1em 0;220 }221222 blockquote {223 border-left: 4px solid #6366f1;224 padding-left: 1em;225 margin-left: 0;226 color: #6b7280;227 font-style: italic;228 }229230 strong {231 font-weight: 600;232 color: #111827;233 }234235 em {236 font-style: italic;237 color: #4b5563;238 }239240 .cover-page {241 display: flex;242 flex-direction: column;243 align-items: center;244 justify-content: center;245 min-height: 70vh;246 text-align: center;247 page-break-after: always;248 }249250 .cover-logo {251 width: 180px;252 height: 180px;253 margin-bottom: 2em;254 }255256 .cover-title {257 font-size: 2.5em;258 font-weight: 700;259 background: linear-gradient(135deg, #119993, #2072B2, #652E89);260 -webkit-background-clip: text;261 -webkit-text-fill-color: transparent;262 margin-bottom: 0.3em;263 }264265 .cover-subtitle {266 font-size: 1.1em;267 color: #6b7280;268 margin-bottom: 2em;269 }270271 .cover-date {272 font-size: 0.9em;273 color: #9ca3af;274 }275276 .cover-divider {277 width: 120px;278 height: 3px;279 background: linear-gradient(90deg, #119993, #2072B2, #652E89);280 border: none;281 margin: 1.5em auto;282 border-radius: 2px;283 }284 </style>285</head>286<body>287 <div class="cover-page">288 <img src="${LOGO_DATA_URI}" class="cover-logo" alt="VQuant" />289 <div class="cover-title">VQuant</div>290 <div class="cover-subtitle">Financial Intelligence Platform</div>291 <hr class="cover-divider" />292 <div class="cover-date">${new Date().toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' })}</div>293 </div>294 <div class="content">295 ${htmlBody}296 </div>297</body>298</html>`;299}300301/**302 * Generate a PDF Buffer from markdown content using Puppeteer303 */304export async function generatePDFFromMarkdown(options: MarkdownPDFOptions): Promise<Buffer> {305 const { question, answer } = options;306307 // Use answer only — no question in PDF308 const markdown = answer;309 const htmlContent = markdownToHTML(markdown, question);310311 let browser;312 try {313 // Launch Puppeteer314 browser = await puppeteer.launch({315 headless: true,316 args: [317 '--no-sandbox',318 '--disable-setuid-sandbox',319 '--disable-dev-shm-usage',320 '--disable-gpu'321 ]322 });323324 const page = await browser.newPage();325326 // Set content and wait for KaTeX to render math327 await page.setContent(htmlContent, {328 waitUntil: 'networkidle0'329 });330 // Extra wait for KaTeX auto-render to finish331 await page.waitForFunction(() => {332 return !document.querySelector('.math-inline, .math-display') ||333 document.querySelector('.katex') !== null;334 }, { timeout: 5000 }).catch(() => {});335336 // Generate PDF337 const pdfData = await page.pdf({338 format: 'A4',339 margin: {340 top: '20mm',341 right: '15mm',342 bottom: '20mm',343 left: '15mm'344 },345 printBackground: true,346 displayHeaderFooter: true,347 headerTemplate: `<div></div>`,348 footerTemplate: `349 <div style="width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 9px; color: #999; padding: 8px 0;">350 <img src="${LOGO_DATA_URI}" style="width: 16px; height: 16px;" />351 <span style="color: #119993; font-weight: 600;">VQuant</span>352 <span style="margin: 0 6px; color: #ddd;">|</span>353 <span>Page <span class="pageNumber"></span> / <span class="totalPages"></span></span>354 <span style="margin: 0 6px; color: #ddd;">|</span>355 <span>${new Date().toLocaleDateString('fr-FR')}</span>356 </div>357 `358 });359360 await browser.close();361362 // Ensure we return a proper Buffer363 const pdfBuffer = Buffer.isBuffer(pdfData) ? pdfData : Buffer.from(pdfData);364 return pdfBuffer;365 } catch (error) {366 if (browser) {367 await browser.close();368 }369 console.error('PDF generation error:', error);370 throw new Error(`Failed to generate PDF: ${error.message}`);371 }372}373374/**375 * Generate a DOCX Buffer from markdown content using Pandoc376 */377export async function generateDOCXFromMarkdown(options: MarkdownPDFOptions): Promise<Buffer> {378 const { question, answer } = options;379380 // Create temp directory if it doesn't exist381 const tempDir = path.join(process.cwd(), 'temp');382 if (!fs.existsSync(tempDir)) {383 fs.mkdirSync(tempDir, { recursive: true });384 }385386 // Generate unique filenames387 const timestamp = Date.now();388 const mdPath = path.join(tempDir, `report-${timestamp}.md`);389 const docxPath = path.join(tempDir, `report-${timestamp}.docx`);390391 try {392 // Use answer only — no question in DOCX393 const markdown = answer;394395 // Write markdown to temp file396 fs.writeFileSync(mdPath, markdown, 'utf-8');397398 // Generate DOCX using Pandoc399 await execAsync(`pandoc "${mdPath}" -f markdown -t docx -o "${docxPath}"`);400401 // Read the generated DOCX file402 const docxBuffer = fs.readFileSync(docxPath);403404 // Clean up temp files405 fs.unlinkSync(mdPath);406 fs.unlinkSync(docxPath);407408 return docxBuffer;409 } catch (error) {410 // Clean up files on error411 if (fs.existsSync(mdPath)) fs.unlinkSync(mdPath);412 if (fs.existsSync(docxPath)) fs.unlinkSync(docxPath);413 throw new Error(`Failed to generate DOCX: ${error.message}`);414 }415}416