SPB Git

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%
28.0 KB · 416 lines typescript
Raw Blame History
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 ![Figure 1](/figures/fig-12345-0.png)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 = `![${altText}](${dataUrl})`;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