generate_b2c_pdf / server.js
Rakshitjan's picture
Update server.js
2f6bb1d verified
// server.js
import fs from "fs";
import path from "path";
import crypto from "crypto";
import fetch from "node-fetch";
import express from "express";
import bodyParser from "body-parser";
import PDFDocument from "pdfkit";
import sharp from "sharp";
import moment from "moment-timezone";
import cors from "cors";
// Configuration (all from environment variables)
const BASE_URL = process.env.BASE_URL || "https://rakshitjan-cps-b2c.hf.space";
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || "admin123@example.com";
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "SecurePass123";
const AES_KEY_STR = process.env.AES_KEY_STR || "7E892875A52C59A3B588306B13C31FBD";
const AES_IV_STR = process.env.AES_IV_STR || "XYE45DKJ0967GFAZ";
const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "AIzaSyBpna0F0J-c9QNBh8lSnfT2Z2uzD_vS9Ag"; // For geocoding
const GEOAPIFY_API_KEY = process.env.GEOAPIFY_API_KEY || "ceaea8a8a8f14f46b73221384eb4b5f0" ; // For static maps
const LOGO_PATH = process.env.LOGO_PATH || "logo.png";
const statelist = JSON.parse(fs.readFileSync("state.json", "utf8"));
const citylist = JSON.parse(fs.readFileSync("city.json", "utf8"));
const districtlist = JSON.parse(fs.readFileSync("district.json", "utf8"));
const organizationlist = JSON.parse(fs.readFileSync("organization.json", "utf8"));
const genderlist = JSON.parse(fs.readFileSync("gender.json", "utf8"));
const qualificationlist = JSON.parse(fs.readFileSync("qualification.json", "utf8"));
const ownershiplist = JSON.parse(fs.readFileSync("ownership.json", "utf8"));
const app = express();
app.use(bodyParser.json({ limit: "50mb" }));
// Allow ALL origins
app.use(cors());
// Utility: convert cm to PDF points (1 pt = 1/72 in; 1 cm = 28.3464567 pt)
const CM = 28.3464567;
const A4_WIDTH = 595.2755905511812; // points (210mm)
const A4_HEIGHT = 841.8897637795277; // points (297mm)
const MARGIN = 1.2 * CM;
// AES encryption like Python's Crypto.Cipher AES-CBC w/ PKCS7 padding
function aesEncrypt(plaintext, keyStr, ivStr) {
// Validate inputs
if (!plaintext) throw new Error("Plaintext is required for encryption");
if (!keyStr) throw new Error("AES key is required");
if (!ivStr) throw new Error("AES IV is required");
const key = Buffer.from(keyStr, "utf8");
const iv = Buffer.from(ivStr, "utf8");
// Validate key and IV lengths
if (key.length !== 16 && key.length !== 24 && key.length !== 32) {
throw new Error(`Invalid AES key length: ${key.length} bytes. Must be 16, 24, or 32 bytes.`);
}
// Node's createCipheriv uses PKCS#7 padding by default (called PKCS#5 in OpenSSL)
const cipher = crypto.createCipheriv(`aes-${key.length * 8}-cbc`, key, iv);
const encrypted = Buffer.concat([
cipher.update(Buffer.from(plaintext, "utf8")),
cipher.final()
]);
return encrypted.toString("base64");
}
// HTTP session mimic (we reuse fetch)
let token = null;
async function adminLogin() {
try {
// encrypt email same as python
const encryptedEmail = aesEncrypt(ADMIN_EMAIL, AES_KEY_STR, AES_IV_STR);
const payload = {
email: encryptedEmail,
password: ADMIN_PASSWORD
};
console.log(`Logging in to ${BASE_URL} with admin email...`);
const res = await fetch(`${BASE_URL}/api/admin/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
timeout: 30000
});
const data = await res.json();
if (res.status !== 200 || !data.token) {
console.error(`Login failed. Status: ${res.status}, Response:`, data);
throw new Error(`Login failed: ${JSON.stringify(data)}`);
}
token = data.token;
console.log("Admin login successful");
return token;
} catch (error) {
console.error("Admin login error:", error.message);
throw error;
}
}
async function getCase(caseId) {
if (!token) await adminLogin();
console.log(`Fetching case ${caseId}...`);
const res = await fetch(`${BASE_URL}/api/cases/${caseId}`, {
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
timeout: 30000
});
if (!res.ok) {
console.error(`Failed to get case ${caseId}: ${res.status} ${res.statusText}`);
throw new Error(`Failed get_case: ${res.status}`);
}
return await res.json();
}
async function getFiles(caseId) {
if (!token) await adminLogin();
console.log(`Fetching files for case ${caseId}...`);
const res = await fetch(`${BASE_URL}/api/cases/${caseId}/files`, {
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
timeout: 60000
});
if (!res.ok) {
console.error(`Failed to get files for case ${caseId}: ${res.status} ${res.statusText}`);
throw new Error(`Failed get_files: ${res.status}`);
}
const json = await res.json();
console.log(`Retrieved ${json.files?.length || 0} files for case ${caseId}`);
return json.files || [];
}
async function downloadFile(fileId) {
if (!token) await adminLogin();
console.log(`Downloading file ${fileId}...`);
const res = await fetch(`${BASE_URL}/api/files/${fileId}`, {
headers: { "Authorization": `Bearer ${token}` },
timeout: 60000
});
if (!res.ok) {
console.error(`File download failed for ${fileId}: ${res.status} ${res.statusText}`);
throw new Error(`File download failed for ${fileId}: ${res.status}`);
}
const buf = await res.arrayBuffer();
return Buffer.from(buf);
}
// Google Maps Geocoding API helpers (for forward/reverse geocoding)
async function forwardGeocode(address) {
if (!address) {
console.log("No address provided for geocoding");
return [null, null];
}
if (!GOOGLE_MAPS_API_KEY) {
console.error("GOOGLE_MAPS_API_KEY is not set");
return [null, null];
}
console.log(`Geocoding address: ${address.substring(0, 50)}...`);
try {
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${GOOGLE_MAPS_API_KEY}`;
const res = await fetch(url, { timeout: 20000 });
if (!res.ok) {
console.error(`Geocoding API error: ${res.status} ${res.statusText}`);
return [null, null];
}
const json = await res.json();
if (json.status === "OK" && json.results && json.results[0] && json.results[0].geometry && json.results[0].geometry.location) {
const loc = json.results[0].geometry.location;
console.log(`Geocoding successful: ${loc.lat}, ${loc.lng}`);
return [loc.lat, loc.lng];
} else {
console.warn(`Geocoding failed: ${json.status} - ${json.error_message || 'No results'}`);
return [null, null];
}
} catch (error) {
console.error("Geocoding error:", error.message);
return [null, null];
}
}
async function reverseGeocode(lat, lon) {
if (lat == null || lon == null) {
console.log("No coordinates provided for reverse geocoding");
return "Unknown Address";
}
if (!GOOGLE_MAPS_API_KEY) {
console.error("GOOGLE_MAPS_API_KEY is not set");
return "Unknown Address";
}
console.log(`Reverse geocoding coordinates: ${lat}, ${lon}`);
try {
const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lon}&key=${GOOGLE_MAPS_API_KEY}`;
const res = await fetch(url, { timeout: 20000 });
if (!res.ok) {
console.error(`Reverse geocoding API error: ${res.status} ${res.statusText}`);
return "Unknown Address";
}
const json = await res.json();
if (json.status === "OK" && json.results && json.results[0] && json.results[0].formatted_address) {
const address = json.results[0].formatted_address;
console.log(`Reverse geocoding successful: ${address.substring(0, 50)}...`);
return address;
} else {
console.warn(`Reverse geocoding failed: ${json.status} - ${json.error_message || 'No results'}`);
return "Unknown Address";
}
} catch (error) {
console.error("Reverse geocoding error:", error.message);
return "Unknown Address";
}
}
// Geoapify Static Maps API (for static maps with markers and path)
async function staticMapWithPath(captureLat, captureLng, caseLat = null, caseLng = null, zoom = 17, size = "1800x600") {
if (captureLat == null || captureLng == null) {
console.log("No capture coordinates provided for static map");
return null;
}
if (!GEOAPIFY_API_KEY) {
console.error("GEOAPIFY_API_KEY is not set");
return null;
}
console.log(`Generating static map for: ${captureLat}, ${captureLng}`);
try {
let parts = [
`center=lonlat:${captureLng},${captureLat}`,
`zoom=${zoom}`,
`size=${size}`,
`scaleFactor=2`,
`marker=lonlat:${captureLng},${captureLat};color:%23ff0000;size:large`
];
if (caseLat != null && caseLng != null) {
parts.push(`marker=lonlat:${caseLng},${caseLat};color:%23007bff;size:large`);
parts.push(`path=color:%23ff0000;weight:4|lonlat:${caseLng},${caseLat}|lonlat:${captureLng},${captureLat}`);
}
const url = `https://maps.geoapify.com/v1/staticmap?${parts.join("&")}&apiKey=${GEOAPIFY_API_KEY}`;
const res = await fetch(url, { timeout: 30000 });
if (!res.ok) {
console.error(`Static map API error: ${res.status} ${res.statusText}`);
return null;
}
const buf = Buffer.from(await res.arrayBuffer());
console.log("Static map generated successfully");
return buf;
} catch (error) {
console.error("Static map generation error:", error.message);
return null;
}
}
function haversineKm(lat1, lon1, lat2, lon2) {
if ([lat1, lon1, lat2, lon2].some(v => v == null)) return null;
const R = 6371.0;
const phi1 = lat1 * Math.PI / 180;
const phi2 = lat2 * Math.PI / 180;
const dphi = (lat2 - lat1) * Math.PI / 180;
const dlambda = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dphi / 2) * Math.sin(dphi / 2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(dlambda / 2) * Math.sin(dlambda / 2);
return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
// date formatting to IST similar to fmt_ist in python
function fmtIST(dtStr) {
if (!dtStr) return "N/A";
try {
// Accept Z or offset
const m = moment(dtStr);
const ist = m.tz("Asia/Kolkata").format("DD/MM/YYYY hh:mm:ss A") + " IST";
return ist;
} catch {
return dtStr;
}
}
function exportedOnStr() {
return moment().tz("Asia/Kolkata").format("MMMM DD, YYYY");
}
// Mapping display names
const DISPLAY_NAME = {
"aadhaar_photo": "Photo of Aadhar",
"customer_photo": "Customer's Photo",
"residence_photo": "Residence Photo",
"business_photo": "Business Address Photo"
};
function displayType(fileType) {
return DISPLAY_NAME[fileType] || fileType || "Upload";
}
// create thumbnail similar to Python make_thumb
async function makeThumb(imgBuf, maxW = 860, maxH = 640) {
try {
// produce an RGB JPEG/PNG buffer sized with thumbnail
const img = sharp(imgBuf).rotate().resize({ width: maxW, height: maxH, fit: "inside" }).flatten({ background: { r: 255, g: 255, b: 255 } });
const png = await img.png().toBuffer();
// create bordered image by extending canvas
const bordered = await sharp(png)
.extend({ top: 1, bottom: 1, left: 1, right: 1, background: { r: 170, g: 170, b: 170 } })
.png()
.toBuffer();
return bordered;
} catch (error) {
console.error("Thumbnail creation error:", error.message);
// Return original buffer if thumbnail creation fails
return imgBuf;
}
}
// text wrapping helper that attempts to match Python's draw_wrapped_label_value.
// We measure text widths using doc.widthOfString and output lines manually at exact coordinates.
function drawWrappedLabelValue(doc, x, y, label, text, maxWidthCm = 16.0) {
const maxW = maxWidthCm * CM;
const fontSize = 10;
doc.font("Helvetica").fontSize(fontSize);
const labelStr = `${label}: `;
const labelW = doc.widthOfString(labelStr);
const words = (text || "N/A").toString().split(/\s+/);
let first = true;
let line = "";
let width = 0;
let lines = [];
for (let w of words) {
const w_w = doc.widthOfString(w + " ");
if (first) {
if (labelW + width + w_w < maxW) {
line += w + " ";
width += w_w;
} else {
lines.push(labelStr + line.trim());
first = false;
line = w + " ";
width = w_w;
}
} else {
if (width + w_w < maxW) {
line += w + " ";
width += w_w;
} else {
// indent roughly by label_w/6 characters -> compute pixels
const indentText = " ".repeat(Math.max(0, Math.floor(labelW / (fontSize * 0.6))));
lines.push(indentText + line.trim());
line = w + " ";
width = w_w;
}
}
}
if (first) {
lines.push(labelStr + (line.trim()));
} else {
if (line) {
const indentText = " ".repeat(Math.max(0, Math.floor(labelW / (fontSize * 0.6))));
lines.push(indentText + line.trim());
}
}
for (let ln of lines) {
doc.text(ln, x, y, { lineBreak: false });
y += 0.45 * CM;
}
return y;
}
// Draw header/footer helpers (matching positions & fonts from python)
function drawHeader(doc, title) {
// header blue rectangle at top: height 2.4 cm
doc.save();
doc.rect(0, 0, A4_WIDTH, 2.4 * CM).fill("#0F61A8");
// logo
let titleX = MARGIN;
if (fs.existsSync(LOGO_PATH)) {
try {
doc.image(LOGO_PATH, MARGIN, 0.2 * CM, { width: 2.4 * CM, height: 2.0 * CM });
titleX = MARGIN + 2.6 * CM;
} catch (e) {
console.warn(`Could not load logo from ${LOGO_PATH}:`, e.message);
}
}
doc.fillColor("white").font("Helvetica-Bold").fontSize(15).text(title, titleX, 0.9 * CM, { continued: false });
doc.font("Helvetica").fontSize(10);
const exp = `Exported on ${exportedOnStr()}`;
// draw right-aligned
const w = doc.widthOfString(exp);
doc.fillColor("white").text(exp, A4_WIDTH - MARGIN - w, 1.0 * CM);
doc.fillColor("black");
doc.restore();
}
function drawFooter(doc, page) {
doc.font("Helvetica").fontSize(9).text(`Page ${page}`, A4_WIDTH - MARGIN - 50, A4_HEIGHT - 1.0 * CM);
}
// Build PDF function (mirrors build_pdf in Python)
async function buildPdf(caseObj, files, outPath) {
return new Promise(async (resolve, reject) => {
try {
console.log(`Building PDF for case ${caseObj.case_id} with ${files.length} files...`);
const doc = new PDFDocument({ size: "A4", margin: 0 });
const ws = fs.createWriteStream(outPath);
doc.pipe(ws);
let page = 1;
// prepare address and geocode
const ad = ((caseObj.demographic_details || {}).address_details) || {};
const caseAddr = `${ad.residential_address || ""}, ${ad.city || ""}, ${ad.state || ""} ${ad.pincode || ""}`.trim();
console.log(`Case address: ${caseAddr}`);
const [caseLat, caseLng] = await forwardGeocode(caseAddr);
if (caseLat && caseLng) {
console.log(`Case coordinates: ${caseLat}, ${caseLng}`);
}
const title = `Name : ${caseObj.case_applicant_name || ""}`;
drawHeader(doc, title);
let y = 3.4 * CM;
// Customer Details section
const customerstate = statelist.find(state => state.id === caseObj.residenceDetail.stateId);
const customerdistrict = districtlist.find(district => district.id === caseObj.residenceDetail.districtId);
const customercity = citylist.find(city => city.id === caseObj.residenceDetail.cityId);
const customeraddress=caseObj.residenceDetail.address1 +", "+ caseObj.residenceDetail.address2 +", "+ caseObj.residenceDetail.address3 +", "+customercity.city_name +", "+ customerdistrict.district_name +", "+ customerstate.state_name +", "+ caseObj.residenceDetail.pincode;
y = sectionTitle(doc, "Customer Details", MARGIN, y);
labelValue(doc, MARGIN, y, "Customer Name", caseObj.case_applicant_name); y += 0.6 * CM;
labelValue(doc, MARGIN, y, "Customer ID", caseObj.case_id); y += 0.6 * CM;
labelValue(doc, MARGIN, y, "Phone Number", caseObj.case_applicant_contact); y += 0.6 * CM;
//const emailId = (caseObj.applicantDetail.email || {}).contact_information && (caseObj.demographic_details.contact_information.email_id);
labelValue(doc, MARGIN, y, "Email", caseObj.applicantDetail.email); y += 0.6 * CM;
y = drawWrappedLabelValue(doc, MARGIN, y, "Customer Address", customeraddress, 18.0); y += 0.6 * CM;
// Inspection Summary
y = sectionTitle(doc, "Inspection Summary", MARGIN, y);
doc.font("Helvetica").fontSize(10);
doc.text(`Request Started: ${fmtIST(caseObj.created_at)}`, MARGIN, y); y += 0.5 * CM;
doc.text(`Total Uploads: ${files.length} uploads`, MARGIN, y); y += 0.5 * CM;
doc.text(`Status: ${caseObj.status}`, MARGIN, y); y += 0.5 * CM;
doc.text(`Priority: ${caseObj.priority}`, MARGIN, y); y += 0.7 * CM;
// render static map if caseLat present (attempt)
if (caseLat) {
console.log("Generating overview map...");
const mapBuf = await staticMapWithPath(caseLat, caseLng, caseLat, caseLng, 12);
if (mapBuf) {
// approximate map size used in python: map_h = H/2 + 2*cm
const mapH = A4_HEIGHT / 2 + 2 * CM;
const mapW = A4_WIDTH - 2 * MARGIN;
doc.image(mapBuf, MARGIN, y, { width: mapW, height: mapH });
y += mapH + 0.8 * CM;
}
}
drawFooter(doc, page);
doc.addPage(); page += 1;
// Page 2: thumbnails
drawHeader(doc, title);
y = 3.4 * CM;
doc.font("Helvetica-Bold").fontSize(14).text("Image Thumbnails", MARGIN, y);
y += 0.8 * CM;
const x0 = MARGIN;
const y0 = y;
const colW = (A4_WIDTH - 2 * MARGIN) / 3;
const rowH = 6.1 * CM;
for (let i = 0; i < Math.min(6, files.length); ++i) {
const f = files[i];
console.log(`Creating thumbnail for upload ${i+1} (${f.file_type})...`);
const imgBuf = await makeThumb(f._bin);
const col = i % 3;
const row = Math.floor(i / 3);
const xi = x0 + col * colW + 0.2 * CM;
const yi = y0 + row * rowH + 4.6 * CM;
// draw image within width colW-0.6cm and height 4 cm
try {
doc.image(imgBuf, xi, yi, { width: colW - 0.6 * CM, height: 4.0 * CM, align: "center", valign: "center" });
} catch (e) {
console.warn(`Could not draw thumbnail ${i+1}:`, e.message);
}
doc.font("Helvetica").fontSize(9).text(`Upload ${i + 1}${displayType(f.file_type)}`, xi, yi - 0.3 * CM);
}
drawFooter(doc, page);
doc.addPage(); page += 1;
// Page 3: Demographic, Business, Financial
drawHeader(doc, title);
y = 3.4 * CM;
y = sectionTitle(doc, "Demographic Details", MARGIN, y);
const demo = caseObj.applicantDetail || {};
const ci = demo.contact_information || {};
const pd = demo.personal_details || {};
const gender = genderlist.find(gender => gender.id === demo.genderId);
const qualification = qualificationlist.find(qualification => qualification.id === demo.qualification);
labelValue(doc, MARGIN, y, "Aadhaar Photo Match", demo.aadhaar_photo_match); y += 0.5 * CM;
labelValue(doc, MARGIN, y, "Email ID", demo.email); y += 0.5 * CM;
labelValue(doc, MARGIN, y, "Mobile Number", demo.mobile); y += 0.5 * CM;
labelValue(doc, MARGIN, y, "Gender", gender.gender_name); y += 0.5 * CM;
labelValue(doc, MARGIN, y, "Education", qualification.qualification_name); y += 0.5 * CM;
labelValue(doc, MARGIN, y, "Family Members", demo.numOfDependent); y += 1.0 * CM;
y = sectionTitle(doc, "Business Details", MARGIN, y);
const bd = caseObj.businessDetail || {};
const ei = bd.enterprise_information || {};
const bld = bd.business_location_details || {};
const ba = bd.business_address || {};
const state = statelist.find(state => state.id === bd.stateId);
const district = districtlist.find(district => district.id === bd.districtId);
const city = citylist.find(city => city.id === bd.cityId);
const organization = organizationlist.find(organization => organization.id === bd.organizationType);
//const city = citylist.find(city => city.id === bd.cityId);
labelValue(doc, MARGIN, y, "Enterprise Name", bd.businessName); y += 0.5 * CM;
labelValue(doc, MARGIN, y, "Organization Type", organization.organization_name); y += 0.5 * CM;
labelValue(doc, MARGIN, y, "Business Location Type", bd.businessLocation); y += 0.5 * CM;
labelValue(doc, MARGIN, y, "Address", `${bd.address1} ${bd.address2} ${bd.landmark }`); y += 0.5 * CM;
labelValue(doc, MARGIN, y, "City/District/State/Pincode", `${city.city_name || ""}/ ${district.district_name || ""}/ ${state.state_name || ""} - ${bd.pincode || ""}`); y += 1.0 * CM;
y = sectionTitle(doc, "Financial Details", MARGIN, y);
const fin = caseObj.financial_details || {};
const bfi = fin.business_financial_information || {};
const ffi = fin.family_financial_information || {};
function fmtInr(v) {
try {
if (v == null || v === "" || isNaN(v)) return "N/A";
const amount = parseInt(v);
// Format with Indian numbering system and ₹ symbol
return `₹ ${amount.toLocaleString('en-IN')}`;
} catch {
return "N/A";
}
}
labelValue(doc, MARGIN, y, "Monthly Income", fmtInr(caseObj.incomeExpenditure.monthlyIncome)); y += 0.5 * CM;
labelValue(doc, MARGIN, y, "Monthly Expense", fmtInr(caseObj.familyExpenses.totalExp)); y += 0.5 * CM;
labelValue(doc, MARGIN, y, "Family Income", fmtInr(caseObj.incomeExpenditure.totalIncome)); y += 0.5 * CM;
drawFooter(doc, page);
doc.addPage(); page += 1;
// For each file, a detailed page
for (let idx = 0; idx < files.length; ++idx) {
const f = files[idx];
console.log(`Processing detailed page for upload ${idx+1}...`);
drawHeader(doc, title);
y = 3.4 * CM;
doc.font("Helvetica-Bold").fontSize(14).text(`Upload ${idx + 1}${displayType(f.file_type)}`, MARGIN, y);
y += 0.8 * CM;
// integrity_dashboard image insertion (if present)
try {
const integrityPath = "integrity_dashboard.png";
if (fs.existsSync(integrityPath)) {
const imageWidth = 16 * CM;
const imageHeight = 2.5 * CM;
const centerX = (A4_WIDTH - imageWidth) / 2;
doc.image(integrityPath, centerX, y, { width: imageWidth, height: imageHeight });
y = y + imageHeight + 0.8 * CM;
} else {
y += 0.5 * CM;
}
} catch (e) {
y += 0.5 * CM;
}
const gl = f.geo_location || {};
const lat = gl.lat;
const lng = gl.lng;
// draw the file image on left
console.log(`Creating thumbnail for detailed view ${idx+1}...`);
const imgBuf = await makeThumb(f._bin);
try {
doc.image(imgBuf, MARGIN, y, { width: 8.6 * CM, height: 9.0 * CM });
} catch (e) {
console.warn(`Could not draw main image ${idx+1}:`, e.message);
}
// draw static map for the single upload using Geoapify
if (lat != null && lng != null) {
console.log(`Generating static map for upload ${idx+1}...`);
const mimgBuf = await staticMapWithPath(lat, lng, caseLat, caseLng, 17);
if (mimgBuf) {
try {
doc.image(mimgBuf, 11.0 * CM, y, { width: 8.0 * CM, height: 9.0 * CM });
} catch (e) {
console.warn(`Could not draw static map ${idx+1}:`, e.message);
}
}
}
let yb = y + 10.4 * CM;
// Use Google Maps for reverse geocoding
const rev = await reverseGeocode(lat, lng);
yb = drawWrappedLabelValue(doc, MARGIN, yb, "Location Address", rev, 18.0);
const distKm = (caseLat != null && lat != null) ? haversineKm(caseLat, caseLng, lat, lng) : null;
const distM = distKm ? Math.round(distKm * 1000) : null;
doc.font("Helvetica").fontSize(10);
doc.text(distM ? `Distance from case location: ${distM} meters` : "Distance from case location: N/A", MARGIN, yb);
yb += 0.7 * CM;
const upAt = fmtIST(f.uploaded_at);
doc.text(`Uploaded At: ${upAt}`, MARGIN, yb); yb += 1.0 * CM;
doc.font("Helvetica-Bold").fontSize(12).text("Metadata & Sensor Information", MARGIN, yb); yb += 0.6 * CM;
doc.font("Helvetica").fontSize(10).text(`Server Time: ${upAt}`, MARGIN, yb); yb += 0.6 * CM;
doc.text(`Device Time: ${upAt}`, MARGIN, yb); yb += 0.6 * CM;
doc.text(distM ? `Accuracy: ${distM} meters` : "Distance from case location: N/A", MARGIN, yb); yb += 0.6 * CM;
if (lat != null) {
doc.text(`Geo: Lat ${lat.toFixed(6)}, Lng ${lng.toFixed(6)}`, MARGIN, yb);
} else {
doc.text("Geo: N/A", MARGIN, yb);
}
yb += 0.6 * CM;
drawFooter(doc, page);
doc.addPage(); page += 1;
}
// Timeline page (mirrors python)
console.log("Creating timeline page...");
drawHeader(doc, title);
y = 3.4 * CM;
doc.font("Helvetica-Bold").fontSize(14).text("Inspection Timeline", MARGIN, y); y += 0.8 * CM;
// draw a rounded rect with a pale bg similar to python's setFillColorRGB
doc.save();
doc.roundedRect(MARGIN, y, A4_WIDTH - 2 * MARGIN, 1.1 * CM, 0.3 * CM).fillOpacity(1).fill("#E0F7E6");
doc.fillColor("#1F743F").font("Helvetica-Bold").fontSize(12).text("Ready for Review", MARGIN + 0.6 * CM, y + 0.4 * CM);
doc.fillColor("black");
doc.restore();
y += 2.0 * CM;
const events = [];
function evt(text, ts, sub = null) {
if (ts) events.push({ text, ts, sub });
}
evt("Case Created", caseObj.created_at);
evt("Assigned to Agent", caseObj.assigned_at);
evt("Accepted by Agent", caseObj.accepted_at);
for (let f of files) {
const sub = await reverseGeocode((f.geo_location || {}).lat, (f.geo_location || {}).lng);
evt(`Upload — ${displayType(f.file_type)}`, f.uploaded_at, sub);
}
evt("Case Updated", caseObj.updated_at);
evt("Case Completed", caseObj.completed_at);
function parseTs(s) {
try {
return new Date(s);
} catch {
return new Date(0);
}
}
events.sort((a, b) => parseTs(a.ts) - parseTs(b.ts));
for (let ev of events) {
doc.font("Helvetica").fontSize(10).text(`• ${fmtIST(ev.ts)}`, MARGIN, y); y += 0.45 * CM;
doc.font("Helvetica-Bold").fontSize(11).text(ev.text, MARGIN + 0.6 * CM, y); y += 0.45 * CM;
if (ev.sub) {
doc.font("Helvetica").fontSize(9).fillColor("gray");
y = drawWrappedLabelValue(doc, MARGIN + 0.6 * CM, y, "Uploaded from", ev.sub, 17.0);
doc.fillColor("black");
y += 0.3 * CM;
}
y += 0.3 * CM;
if (y > A4_HEIGHT - 3 * CM) {
drawFooter(doc, page);
doc.addPage(); page += 1;
drawHeader(doc, title);
y = 3.4 * CM;
doc.font("Helvetica-Bold").fontSize(14).text("Inspection Timeline (Cont.)", MARGIN, y); y += 1.0 * CM;
}
}
drawFooter(doc, page);
doc.addPage(); // finalize
doc.end();
ws.on("finish", () => {
console.log(`PDF created successfully at ${outPath}`);
resolve();
});
ws.on("error", (e) => {
console.error("PDF write stream error:", e);
reject(e);
});
} catch (e) {
console.error("PDF build error:", e);
reject(e);
}
});
}
// small helpers to draw section title and label:value
function sectionTitle(doc, text, x, y) {
doc.font("Helvetica-Bold").fontSize(14).text(text, x, y);
return y + 0.9 * CM;
}
function labelValue(doc, x, y, label, value) {
doc.font("Helvetica-Bold").fontSize(10).text(`${label}:`, x, y);
doc.font("Helvetica").fontSize(10).text((value == null || value === "" || (Array.isArray(value) && value.length === 0)) ? "N/A" : value.toString(), x + 4.8 * CM, y);
}
// FastAPI-equivalent endpoints
app.get("/health", (req, res) => {
console.log("Health check");
res.json({
status: "healthy",
env_loaded: !!ADMIN_EMAIL && !!ADMIN_PASSWORD && !!AES_KEY_STR && !!AES_IV_STR
});
});
app.post("/generate-report", async (req, res) => {
console.log("Generate report request received");
try {
const caseId = (req.body || {}).case_id;
if (!caseId) {
console.error("Missing case_id in request");
return res.status(400).json({ error: "case_id required" });
}
console.log(`Generating report for case: ${caseId}`);
await adminLogin();
const caseObj = await getCase(caseId);
const files = await getFiles(caseId);
console.log(caseObj)
console.log(`Downloading ${files.length} files...`);
// download binaries
for (let i = 0; i < files.length; i++) {
console.log(`Downloading file ${i+1}/${files.length}...`);
files[i]._bin = await downloadFile(files[i].file_id);
}
// ensure output dir
const outDir = "/app/output";
if (!fs.existsSync(outDir)) {
console.log(`Creating output directory: ${outDir}`);
fs.mkdirSync(outDir, { recursive: true });
}
const outputFilename = `Report_${caseId}.pdf`;
const outputPath = path.join(outDir, outputFilename);
console.log(`Building PDF at: ${outputPath}`);
await buildPdf(caseObj, files, outputPath);
const pdfContent = fs.readFileSync(outputPath);
// cleanup
try {
fs.unlinkSync(outputPath);
console.log(`Cleaned up temporary file: ${outputPath}`);
} catch { }
console.log(`Sending PDF response: ${outputFilename} (${pdfContent.length} bytes)`);
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Disposition", `attachment; filename="${outputFilename}"`);
res.send(pdfContent);
console.log("PDF sent successfully");
} catch (e) {
console.error("Error generating PDF:", e);
res.status(500).json({
error: `Error generating PDF: ${e.message || String(e)}`,
stack: process.env.NODE_ENV === 'development' ? e.stack : undefined
});
}
});
app.get("/", (req, res) => {
console.log("Root endpoint accessed");
res.json({
message: "Case Report Generator API",
status: "running",
endpoints: {
health: "GET /health",
generate: "POST /generate-report"
}
});
});
const PORT = process.env.PORT || 7860;
app.listen(PORT, "0.0.0.0", () => {
console.log(`Server listening on port ${PORT}`);
console.log(`Environment check:`);
});
// // server.js
// import fs from "fs";
// import path from "path";
// import crypto from "crypto";
// import fetch from "node-fetch";
// import express from "express";
// import bodyParser from "body-parser";
// import PDFDocument from "pdfkit";
// import sharp from "sharp";
// import moment from "moment-timezone";
// import cors from "cors";
// // Configuration (all from environment variables)
// const BASE_URL = process.env.BASE_URL || "https://rakshitjan-cps-b2c.hf.space";
// const ADMIN_EMAIL = process.env.ADMIN_EMAIL || "admin123@example.com";
// const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "SecurePass123";
// const AES_KEY_STR = process.env.AES_KEY_STR || "7E892875A52C59A3B588306B13C31FBD";
// const AES_IV_STR = process.env.AES_IV_STR || "XYE45DKJ0967GFAZ";
// const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "AIzaSyBpna0F0J-c9QNBh8lSnfT2Z2uzD_vS9Ag"; // For geocoding
// const GEOAPIFY_API_KEY = process.env.GEOAPIFY_API_KEY || "ceaea8a8a8f14f46b73221384eb4b5f0" ; // For static maps
// const LOGO_PATH = process.env.LOGO_PATH || "logo.png";
// // PDF compression settings
// const PDF_COMPRESSION_ENABLED = process.env.PDF_COMPRESSION_ENABLED !== "false";
// const IMAGE_COMPRESSION_QUALITY = parseInt(process.env.IMAGE_COMPRESSION_QUALITY) || 65;
// const MAX_IMAGE_DIMENSION = parseInt(process.env.MAX_IMAGE_DIMENSION) || 1200;
// const THUMBNAIL_QUALITY = parseInt(process.env.THUMBNAIL_QUALITY) || 55;
// const app = express();
// app.use(bodyParser.json({ limit: "50mb" }));
// // Allow ALL origins
// app.use(cors());
// // Utility: convert cm to PDF points (1 pt = 1/72 in; 1 cm = 28.3464567 pt)
// const CM = 28.3464567;
// const A4_WIDTH = 595.2755905511812; // points (210mm)
// const A4_HEIGHT = 841.8897637795277; // points (297mm)
// const MARGIN = 1.2 * CM;
// // AES encryption like Python's Crypto.Cipher AES-CBC w/ PKCS7 padding
// function aesEncrypt(plaintext, keyStr, ivStr) {
// // Validate inputs
// if (!plaintext) throw new Error("Plaintext is required for encryption");
// if (!keyStr) throw new Error("AES key is required");
// if (!ivStr) throw new Error("AES IV is required");
// const key = Buffer.from(keyStr, "utf8");
// const iv = Buffer.from(ivStr, "utf8");
// // Validate key and IV lengths
// if (key.length !== 16 && key.length !== 24 && key.length !== 32) {
// throw new Error(`Invalid AES key length: ${key.length} bytes. Must be 16, 24, or 32 bytes.`);
// }
// // Node's createCipheriv uses PKCS#7 padding by default (called PKCS#5 in OpenSSL)
// const cipher = crypto.createCipheriv(`aes-${key.length * 8}-cbc`, key, iv);
// const encrypted = Buffer.concat([
// cipher.update(Buffer.from(plaintext, "utf8")),
// cipher.final()
// ]);
// return encrypted.toString("base64");
// }
// // HTTP session mimic (we reuse fetch)
// let token = null;
// async function adminLogin() {
// try {
// // encrypt email same as python
// const encryptedEmail = aesEncrypt(ADMIN_EMAIL, AES_KEY_STR, AES_IV_STR);
// const payload = {
// email: encryptedEmail,
// password: ADMIN_PASSWORD
// };
// console.log(`Logging in to ${BASE_URL} with admin email...`);
// const res = await fetch(`${BASE_URL}/api/admin/login`, {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify(payload),
// timeout: 30000
// });
// const data = await res.json();
// if (res.status !== 200 || !data.token) {
// console.error(`Login failed. Status: ${res.status}, Response:`, data);
// throw new Error(`Login failed: ${JSON.stringify(data)}`);
// }
// token = data.token;
// console.log("Admin login successful");
// return token;
// } catch (error) {
// console.error("Admin login error:", error.message);
// throw error;
// }
// }
// async function getCase(caseId) {
// if (!token) await adminLogin();
// console.log(`Fetching case ${caseId}...`);
// const res = await fetch(`${BASE_URL}/api/cases/${caseId}`, {
// headers: {
// "Content-Type": "application/json",
// "Authorization": `Bearer ${token}`
// },
// timeout: 30000
// });
// if (!res.ok) {
// console.error(`Failed to get case ${caseId}: ${res.status} ${res.statusText}`);
// throw new Error(`Failed get_case: ${res.status}`);
// }
// return await res.json();
// }
// async function getFiles(caseId) {
// if (!token) await adminLogin();
// console.log(`Fetching files for case ${caseId}...`);
// const res = await fetch(`${BASE_URL}/api/cases/${caseId}/files`, {
// headers: {
// "Content-Type": "application/json",
// "Authorization": `Bearer ${token}`
// },
// timeout: 60000
// });
// if (!res.ok) {
// console.error(`Failed to get files for case ${caseId}: ${res.status} ${res.statusText}`);
// throw new Error(`Failed get_files: ${res.status}`);
// }
// const json = await res.json();
// console.log(`Retrieved ${json.files?.length || 0} files for case ${caseId}`);
// return json.files || [];
// }
// async function downloadFile(fileId) {
// if (!token) await adminLogin();
// console.log(`Downloading file ${fileId}...`);
// const res = await fetch(`${BASE_URL}/api/files/${fileId}`, {
// headers: { "Authorization": `Bearer ${token}` },
// timeout: 60000
// });
// if (!res.ok) {
// console.error(`File download failed for ${fileId}: ${res.status} ${res.statusText}`);
// throw new Error(`File download failed for ${fileId}: ${res.status}`);
// }
// const buf = await res.arrayBuffer();
// return Buffer.from(buf);
// }
// // Google Maps Geocoding API helpers (for forward/reverse geocoding)
// async function forwardGeocode(address) {
// if (!address) {
// console.log("No address provided for geocoding");
// return [null, null];
// }
// if (!GOOGLE_MAPS_API_KEY) {
// console.error("GOOGLE_MAPS_API_KEY is not set");
// return [null, null];
// }
// console.log(`Geocoding address: ${address.substring(0, 50)}...`);
// try {
// const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${GOOGLE_MAPS_API_KEY}`;
// const res = await fetch(url, { timeout: 20000 });
// if (!res.ok) {
// console.error(`Geocoding API error: ${res.status} ${res.statusText}`);
// return [null, null];
// }
// const json = await res.json();
// if (json.status === "OK" && json.results && json.results[0] && json.results[0].geometry && json.results[0].geometry.location) {
// const loc = json.results[0].geometry.location;
// console.log(`Geocoding successful: ${loc.lat}, ${loc.lng}`);
// return [loc.lat, loc.lng];
// } else {
// console.warn(`Geocoding failed: ${json.status} - ${json.error_message || 'No results'}`);
// return [null, null];
// }
// } catch (error) {
// console.error("Geocoding error:", error.message);
// return [null, null];
// }
// }
// async function reverseGeocode(lat, lon) {
// if (lat == null || lon == null) {
// console.log("No coordinates provided for reverse geocoding");
// return "Unknown Address";
// }
// if (!GOOGLE_MAPS_API_KEY) {
// console.error("GOOGLE_MAPS_API_KEY is not set");
// return "Unknown Address";
// }
// console.log(`Reverse geocoding coordinates: ${lat}, ${lon}`);
// try {
// const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lon}&key=${GOOGLE_MAPS_API_KEY}`;
// const res = await fetch(url, { timeout: 20000 });
// if (!res.ok) {
// console.error(`Reverse geocoding API error: ${res.status} ${res.statusText}`);
// return "Unknown Address";
// }
// const json = await res.json();
// if (json.status === "OK" && json.results && json.results[0] && json.results[0].formatted_address) {
// const address = json.results[0].formatted_address;
// console.log(`Reverse geocoding successful: ${address.substring(0, 50)}...`);
// return address;
// } else {
// console.warn(`Reverse geocoding failed: ${json.status} - ${json.error_message || 'No results'}`);
// return "Unknown Address";
// }
// } catch (error) {
// console.error("Reverse geocoding error:", error.message);
// return "Unknown Address";
// }
// }
// // Geoapify Static Maps API (for static maps with markers and path)
// async function staticMapWithPath(captureLat, captureLng, caseLat = null, caseLng = null, zoom = 17, size = "1800x600") {
// if (captureLat == null || captureLng == null) {
// console.log("No capture coordinates provided for static map");
// return null;
// }
// if (!GEOAPIFY_API_KEY) {
// console.error("GEOAPIFY_API_KEY is not set");
// return null;
// }
// console.log(`Generating static map for: ${captureLat}, ${captureLng}`);
// try {
// let parts = [
// `center=lonlat:${captureLng},${captureLat}`,
// `zoom=${zoom}`,
// `size=${size}`,
// `scaleFactor=2`,
// `marker=lonlat:${captureLng},${captureLat};color:%23ff0000;size:large`
// ];
// if (caseLat != null && caseLng != null) {
// parts.push(`marker=lonlat:${caseLng},${caseLat};color:%23007bff;size:large`);
// parts.push(`path=color:%23ff0000;weight:4|lonlat:${caseLng},${caseLat}|lonlat:${captureLng},${captureLat}`);
// }
// const url = `https://maps.geoapify.com/v1/staticmap?${parts.join("&")}&apiKey=${GEOAPIFY_API_KEY}`;
// const res = await fetch(url, { timeout: 30000 });
// if (!res.ok) {
// console.error(`Static map API error: ${res.status} ${res.statusText}`);
// return null;
// }
// const buf = Buffer.from(await res.arrayBuffer());
// console.log("Static map generated successfully");
// return buf;
// } catch (error) {
// console.error("Static map generation error:", error.message);
// return null;
// }
// }
// function haversineKm(lat1, lon1, lat2, lon2) {
// if ([lat1, lon1, lat2, lon2].some(v => v == null)) return null;
// const R = 6371.0;
// const phi1 = lat1 * Math.PI / 180;
// const phi2 = lat2 * Math.PI / 180;
// const dphi = (lat2 - lat1) * Math.PI / 180;
// const dlambda = (lon2 - lon1) * Math.PI / 180;
// const a = Math.sin(dphi / 2) * Math.sin(dphi / 2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(dlambda / 2) * Math.sin(dlambda / 2);
// return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
// }
// // date formatting to IST similar to fmt_ist in python
// function fmtIST(dtStr) {
// if (!dtStr) return "N/A";
// try {
// // Accept Z or offset
// const m = moment(dtStr);
// const ist = m.tz("Asia/Kolkata").format("DD/MM/YYYY hh:mm:ss A") + " IST";
// return ist;
// } catch {
// return dtStr;
// }
// }
// function exportedOnStr() {
// return moment().tz("Asia/Kolkata").format("MMMM DD, YYYY");
// }
// // Mapping display names
// const DISPLAY_NAME = {
// "aadhaar_photo": "Photo of Aadhar",
// "customer_photo": "Customer's Photo",
// "residence_photo": "Residence Photo",
// "business_photo": "Business Address Photo"
// };
// function displayType(fileType) {
// return DISPLAY_NAME[fileType] || fileType || "Upload";
// }
// // Optimized image compression - resize and compress aggressively
// async function compressImageBuffer(imageBuffer, quality = IMAGE_COMPRESSION_QUALITY, maxDimension = MAX_IMAGE_DIMENSION) {
// try {
// // Get image info
// const image = sharp(imageBuffer);
// const metadata = await image.metadata();
// // Check if image is already small
// if (imageBuffer.length < 100 * 1024) { // Less than 100KB
// console.log(`Image already small (${Math.round(imageBuffer.length / 1024)}KB), skipping compression`);
// return imageBuffer;
// }
// console.log(`Compressing image: ${Math.round(imageBuffer.length / 1024)}KB, ${metadata.width}x${metadata.height}`);
// // Calculate new dimensions if needed
// let newWidth = metadata.width;
// let newHeight = metadata.height;
// if (metadata.width > maxDimension || metadata.height > maxDimension) {
// if (metadata.width > metadata.height) {
// newWidth = maxDimension;
// newHeight = Math.round((metadata.height / metadata.width) * maxDimension);
// } else {
// newHeight = maxDimension;
// newWidth = Math.round((metadata.width / metadata.height) * maxDimension);
// }
// console.log(`Resizing image from ${metadata.width}x${metadata.height} to ${newWidth}x${newHeight}`);
// }
// // Use WebP for best compression (much better than JPEG for same quality)
// const compressedBuffer = await sharp(imageBuffer)
// .resize(newWidth, newHeight, {
// fit: 'inside',
// withoutEnlargement: true
// })
// .webp({
// quality: Math.max(quality, 50), // Minimum 50% quality
// effort: 6, // Maximum compression effort (slowest but best)
// lossless: false
// })
// .toBuffer()
// .catch(async (webpError) => {
// console.log(`WebP failed, using JPEG: ${webpError.message}`);
// // Fallback to JPEG with mozjpeg for better compression
// return await sharp(imageBuffer)
// .resize(newWidth, newHeight, {
// fit: 'inside',
// withoutEnlargement: true
// })
// .flatten({ background: { r: 255, g: 255, b: 255 } }) // Remove transparency
// .jpeg({
// quality: Math.max(quality - 10, 40), // JPEG needs lower quality for similar size
// mozjpeg: true, // Better compression
// chromaSubsampling: '4:4:4' // Better quality
// })
// .toBuffer();
// });
// const originalSize = imageBuffer.length;
// const compressedSize = compressedBuffer.length;
// const compressionRatio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1);
// console.log(`Image compressed: ${Math.round(originalSize / 1024)}KB → ${Math.round(compressedSize / 1024)}KB (${compressionRatio}% reduction)`);
// return compressedBuffer;
// } catch (error) {
// console.error("Image compression error:", error.message);
// // Return original buffer if compression fails
// return imageBuffer;
// }
// }
// // Optimized thumbnail creation with aggressive compression
// async function makeThumb(imgBuf, maxW = 860, maxH = 640) {
// try {
// // First compress and resize the image aggressively for thumbnails
// const compressedImg = await compressImageBuffer(imgBuf, THUMBNAIL_QUALITY, Math.max(maxW, maxH));
// // Create thumbnail with specific dimensions
// const img = sharp(compressedImg)
// .rotate()
// .resize({
// width: maxW,
// height: maxH,
// fit: "inside",
// withoutEnlargement: true,
// kernel: sharp.kernel.lanczos3 // Better downsampling
// })
// .flatten({ background: { r: 255, g: 255, b: 255 } });
// // Use WebP for thumbnails
// let outputBuffer;
// try {
// outputBuffer = await img
// .webp({
// quality: THUMBNAIL_QUALITY,
// effort: 6,
// lossless: false
// })
// .toBuffer();
// } catch {
// // Fallback to JPEG
// outputBuffer = await img
// .jpeg({
// quality: THUMBNAIL_QUALITY - 10,
// mozjpeg: true
// })
// .toBuffer();
// }
// // Add minimal border
// const bordered = await sharp(outputBuffer)
// .extend({
// top: 1,
// bottom: 1,
// left: 1,
// right: 1,
// background: { r: 170, g: 170, b: 170 }
// });
// let finalBuffer;
// try {
// finalBuffer = await bordered
// .webp({
// quality: THUMBNAIL_QUALITY,
// effort: 6
// })
// .toBuffer();
// } catch {
// finalBuffer = await bordered
// .jpeg({
// quality: THUMBNAIL_QUALITY - 10,
// mozjpeg: true
// })
// .toBuffer();
// }
// return finalBuffer;
// } catch (error) {
// console.error("Thumbnail creation error:", error.message);
// // Return original if thumbnail creation fails
// return await compressImageBuffer(imgBuf, THUMBNAIL_QUALITY);
// }
// }
// // Compress static maps aggressively (maps don't need high quality)
// async function compressStaticMap(mapBuffer) {
// if (!mapBuffer) return mapBuffer;
// try {
// const originalSize = mapBuffer.length;
// // If already small, return as is
// if (originalSize < 50 * 1024) {
// return mapBuffer;
// }
// console.log(`Compressing static map: ${Math.round(originalSize / 1024)}KB`);
// // Static maps can be heavily compressed
// const compressedBuffer = await sharp(mapBuffer)
// .webp({
// quality: 50, // Low quality for maps
// effort: 6,
// lossless: false
// })
// .toBuffer()
// .catch(() => {
// // Fallback to JPEG
// return sharp(mapBuffer)
// .jpeg({
// quality: 55,
// mozjpeg: true
// })
// .toBuffer();
// });
// const compressedSize = compressedBuffer.length;
// const compressionRatio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1);
// console.log(`Static map compressed: ${Math.round(originalSize / 1024)}KB → ${Math.round(compressedSize / 1024)}KB (${compressionRatio}% reduction)`);
// return compressedBuffer;
// } catch (error) {
// console.error("Static map compression error:", error.message);
// return mapBuffer;
// }
// }
// // Simple PDF optimization - we rely on image compression and PDFKit's built-in compression
// async function optimizePDF(inputPath, outputPath) {
// if (!PDF_COMPRESSION_ENABLED) {
// fs.copyFileSync(inputPath, outputPath);
// return outputPath;
// }
// try {
// // For Hugging Face, we can't use Ghostscript, so we rely on the compression
// // we've already done at the image level and PDFKit's built-in compression
// console.log("Using image-level compression (optimized for Hugging Face)");
// // Just copy the file - compression was already done during creation
// fs.copyFileSync(inputPath, outputPath);
// return outputPath;
// } catch (error) {
// console.error("PDF optimization failed:", error.message);
// // Fallback to original
// fs.copyFileSync(inputPath, outputPath);
// return outputPath;
// }
// }
// // text wrapping helper that attempts to match Python's draw_wrapped_label_value.
// function drawWrappedLabelValue(doc, x, y, label, text, maxWidthCm = 16.0) {
// const maxW = maxWidthCm * CM;
// const fontSize = 10;
// doc.font("Helvetica").fontSize(fontSize);
// const labelStr = `${label}: `;
// const labelW = doc.widthOfString(labelStr);
// const words = (text || "N/A").toString().split(/\s+/);
// let first = true;
// let line = "";
// let width = 0;
// let lines = [];
// for (let w of words) {
// const w_w = doc.widthOfString(w + " ");
// if (first) {
// if (labelW + width + w_w < maxW) {
// line += w + " ";
// width += w_w;
// } else {
// lines.push(labelStr + line.trim());
// first = false;
// line = w + " ";
// width = w_w;
// }
// } else {
// if (width + w_w < maxW) {
// line += w + " ";
// width += w_w;
// } else {
// const indentText = " ".repeat(Math.max(0, Math.floor(labelW / (fontSize * 0.6))));
// lines.push(indentText + line.trim());
// line = w + " ";
// width = w_w;
// }
// }
// }
// if (first) {
// lines.push(labelStr + (line.trim()));
// } else {
// if (line) {
// const indentText = " ".repeat(Math.max(0, Math.floor(labelW / (fontSize * 0.6))));
// lines.push(indentText + line.trim());
// }
// }
// for (let ln of lines) {
// doc.text(ln, x, y, { lineBreak: false });
// y += 0.45 * CM;
// }
// return y;
// }
// // Draw header/footer helpers
// function drawHeader(doc, title) {
// // header blue rectangle at top: height 2.4 cm
// doc.save();
// doc.rect(0, 0, A4_WIDTH, 2.4 * CM).fill("#0F61A8");
// // logo
// let titleX = MARGIN;
// if (fs.existsSync(LOGO_PATH)) {
// try {
// const logoBuffer = fs.readFileSync(LOGO_PATH);
// doc.image(logoBuffer, MARGIN, 0.2 * CM, { width: 2.4 * CM, height: 2.0 * CM });
// titleX = MARGIN + 2.6 * CM;
// } catch (e) {
// console.warn(`Could not load logo from ${LOGO_PATH}:`, e.message);
// }
// }
// doc.fillColor("white").font("Helvetica-Bold").fontSize(15).text(title, titleX, 0.9 * CM, { continued: false });
// doc.font("Helvetica").fontSize(10);
// const exp = `Exported on ${exportedOnStr()}`;
// const w = doc.widthOfString(exp);
// doc.fillColor("white").text(exp, A4_WIDTH - MARGIN - w, 1.0 * CM);
// doc.fillColor("black");
// doc.restore();
// }
// function drawFooter(doc, page) {
// doc.font("Helvetica").fontSize(9).text(`Page ${page}`, A4_WIDTH - MARGIN - 50, A4_HEIGHT - 1.0 * CM);
// }
// // Optimized Build PDF function
// async function buildPdf(caseObj, files, outPath) {
// return new Promise(async (resolve, reject) => {
// try {
// console.log(`Building optimized PDF for case ${caseObj.case_id} with ${files.length} files...`);
// const doc = new PDFDocument({
// size: "A4",
// margin: 0,
// compress: true, // Enable PDF compression
// pdfVersion: '1.5', // Better compression
// info: {
// Title: `Report for ${caseObj.case_applicant_name || ''}`,
// Author: 'Case Report System',
// Subject: 'Case Inspection Report',
// Keywords: 'case,report,inspection',
// Creator: 'PDFKit with Compression',
// Producer: 'Hugging Face Optimized',
// CreationDate: new Date()
// }
// });
// const ws = fs.createWriteStream(outPath);
// doc.pipe(ws);
// let page = 1;
// const ad = ((caseObj.demographic_details || {}).address_details) || {};
// const caseAddr = `${ad.residential_address || ""}, ${ad.city || ""}, ${ad.state || ""} ${ad.pincode || ""}`.trim();
// console.log(`Case address: ${caseAddr}`);
// const [caseLat, caseLng] = await forwardGeocode(caseAddr);
// if (caseLat && caseLng) {
// console.log(`Case coordinates: ${caseLat}, ${caseLng}`);
// }
// const title = `Name : ${caseObj.case_applicant_name || ""}`;
// drawHeader(doc, title);
// let y = 3.4 * CM;
// // Customer Details section
// y = sectionTitle(doc, "Customer Details", MARGIN, y);
// labelValue(doc, MARGIN, y, "Customer Name", caseObj.case_applicant_name); y += 0.6 * CM;
// labelValue(doc, MARGIN, y, "Customer ID", caseObj.case_id); y += 0.6 * CM;
// labelValue(doc, MARGIN, y, "Phone Number", caseObj.case_applicant_contact); y += 0.6 * CM;
// const emailId = (caseObj.demographic_details || {}).contact_information && (caseObj.demographic_details.contact_information.email_id);
// labelValue(doc, MARGIN, y, "Email", emailId); y += 0.6 * CM;
// y = drawWrappedLabelValue(doc, MARGIN, y, "Customer Address", caseAddr, 18.0); y += 0.4 * CM;
// // Inspection Summary
// y = sectionTitle(doc, "Inspection Summary", MARGIN, y);
// doc.font("Helvetica").fontSize(10);
// doc.text(`Request Started: ${fmtIST(caseObj.created_at)}`, MARGIN, y); y += 0.5 * CM;
// doc.text(`Total Uploads: ${files.length} uploads`, MARGIN, y); y += 0.5 * CM;
// doc.text(`Status: ${caseObj.status}`, MARGIN, y); y += 0.5 * CM;
// doc.text(`Priority: ${caseObj.priority}`, MARGIN, y); y += 0.7 * CM;
// // render static map if caseLat present
// if (caseLat) {
// console.log("Generating and compressing overview map...");
// const mapBuf = await staticMapWithPath(caseLat, caseLng, caseLat, caseLng, 12, "1200x400"); // Smaller size
// if (mapBuf) {
// const compressedMapBuf = await compressStaticMap(mapBuf);
// const mapH = 8 * CM; // Smaller height
// const mapW = A4_WIDTH - 2 * MARGIN;
// doc.image(compressedMapBuf, MARGIN, y, { width: mapW, height: mapH });
// y += mapH + 0.8 * CM;
// }
// }
// drawFooter(doc, page);
// doc.addPage(); page += 1;
// // Page 2: thumbnails (limited to 6 for size)
// drawHeader(doc, title);
// y = 3.4 * CM;
// doc.font("Helvetica-Bold").fontSize(14).text("Image Thumbnails", MARGIN, y);
// y += 0.8 * CM;
// const x0 = MARGIN;
// const y0 = y;
// const colW = (A4_WIDTH - 2 * MARGIN) / 3;
// const rowH = 5.5 * CM; // Smaller row height
// // Limit thumbnails to 6 max for file size
// const maxThumbnails = Math.min(6, files.length);
// for (let i = 0; i < maxThumbnails; ++i) {
// const f = files[i];
// console.log(`Creating optimized thumbnail for upload ${i+1}...`);
// const imgBuf = await makeThumb(f._bin);
// const col = i % 3;
// const row = Math.floor(i / 3);
// const xi = x0 + col * colW + 0.2 * CM;
// const yi = y0 + row * rowH + 4.0 * CM; // Adjusted position
// try {
// doc.image(imgBuf, xi, yi, { width: colW - 0.8 * CM, height: 3.5 * CM }); // Smaller thumbnails
// } catch (e) {
// console.warn(`Could not draw thumbnail ${i+1}:`, e.message);
// }
// doc.font("Helvetica").fontSize(8).text(`Upload ${i + 1} — ${displayType(f.file_type)}`, xi, yi - 0.3 * CM);
// }
// drawFooter(doc, page);
// doc.addPage(); page += 1;
// // Page 3: Demographic, Business, Financial
// drawHeader(doc, title);
// y = 3.4 * CM;
// y = sectionTitle(doc, "Demographic Details", MARGIN, y);
// const demo = caseObj.demographic_details || {};
// const ci = demo.contact_information || {};
// const pd = demo.personal_details || {};
// labelValue(doc, MARGIN, y, "Aadhaar Photo Match", demo.aadhaar_photo_match); y += 0.5 * CM;
// labelValue(doc, MARGIN, y, "Email ID", ci.email_id); y += 0.5 * CM;
// labelValue(doc, MARGIN, y, "Mobile Number", ci.mobile_number); y += 0.5 * CM;
// labelValue(doc, MARGIN, y, "Gender", pd.gender); y += 0.5 * CM;
// labelValue(doc, MARGIN, y, "Education", pd.education); y += 0.5 * CM;
// labelValue(doc, MARGIN, y, "Family Members", pd.number_of_family_members); y += 1.0 * CM;
// y = sectionTitle(doc, "Business Details", MARGIN, y);
// const bd = caseObj.business_details || {};
// const ei = bd.enterprise_information || {};
// const bld = bd.business_location_details || {};
// const ba = bd.business_address || {};
// labelValue(doc, MARGIN, y, "Enterprise Name", ei.enterprise_name); y += 0.5 * CM;
// labelValue(doc, MARGIN, y, "Organization Type", ei.type_of_organization); y += 0.5 * CM;
// labelValue(doc, MARGIN, y, "Business Location Type", bld.business_location); y += 0.5 * CM;
// labelValue(doc, MARGIN, y, "Address", ba.address); y += 0.5 * CM;
// labelValue(doc, MARGIN, y, "City/District/State/Pincode", `${ba.city || ""}, ${ba.district || ""}, ${ba.state || ""} - ${ba.pincode || ""}`); y += 1.0 * CM;
// y = sectionTitle(doc, "Financial Details", MARGIN, y);
// const fin = caseObj.financial_details || {};
// const bfi = fin.business_financial_information || {};
// const ffi = fin.family_financial_information || {};
// function fmtInr(v) {
// try {
// if (v == null || v === "" || isNaN(v)) return "N/A";
// const amount = parseInt(v);
// return `₹ ${amount.toLocaleString('en-IN')}`;
// } catch {
// return "N/A";
// }
// }
// labelValue(doc, MARGIN, y, "Monthly Income", fmtInr(bfi.monthly_income_from_business)); y += 0.5 * CM;
// labelValue(doc, MARGIN, y, "Monthly Expense", fmtInr(bfi.monthly_expense_of_business)); y += 0.5 * CM;
// labelValue(doc, MARGIN, y, "Family Income", fmtInr(ffi.monthly_family_income)); y += 0.5 * CM;
// drawFooter(doc, page);
// doc.addPage(); page += 1;
// // For each file, a detailed page (with aggressive image compression)
// for (let idx = 0; idx < files.length; ++idx) {
// const f = files[idx];
// console.log(`Processing detailed page for upload ${idx+1}...`);
// drawHeader(doc, title);
// y = 3.4 * CM;
// doc.font("Helvetica-Bold").fontSize(14).text(`Upload ${idx + 1} — ${displayType(f.file_type)}`, MARGIN, y);
// y += 0.8 * CM;
// // integrity_dashboard image (skip if not critical)
// try {
// const integrityPath = "integrity_dashboard.png";
// if (fs.existsSync(integrityPath) && fs.statSync(integrityPath).size < 100 * 1024) {
// const imageWidth = 16 * CM;
// const imageHeight = 2.5 * CM;
// const centerX = (A4_WIDTH - imageWidth) / 2;
// doc.image(integrityPath, centerX, y, { width: imageWidth, height: imageHeight });
// y = y + imageHeight + 0.8 * CM;
// } else {
// y += 0.5 * CM;
// }
// } catch (e) {
// y += 0.5 * CM;
// }
// const gl = f.geo_location || {};
// const lat = gl.lat;
// const lng = gl.lng;
// // Main image with aggressive compression
// console.log(`Creating compressed main image for upload ${idx+1}...`);
// const imgBuf = await compressImageBuffer(f._bin, IMAGE_COMPRESSION_QUALITY - 10, 1000);
// try {
// doc.image(imgBuf, MARGIN, y, { width: 8.0 * CM, height: 8.0 * CM }); // Smaller size
// } catch (e) {
// console.warn(`Could not draw main image ${idx+1}:`, e.message);
// }
// // Static map (only if coordinates exist)
// if (lat != null && lng != null) {
// console.log(`Generating compressed static map for upload ${idx+1}...`);
// const mimgBuf = await staticMapWithPath(lat, lng, caseLat, caseLng, 17, "800x600"); // Smaller map
// if (mimgBuf) {
// const compressedMapBuf = await compressStaticMap(mimgBuf);
// try {
// doc.image(compressedMapBuf, 10.5 * CM, y, { width: 7.5 * CM, height: 7.5 * CM }); // Smaller
// } catch (e) {
// console.warn(`Could not draw static map ${idx+1}:`, e.message);
// }
// }
// }
// let yb = y + 9.0 * CM; // Adjusted for smaller images
// // Reverse geocode
// const rev = await reverseGeocode(lat, lng);
// yb = drawWrappedLabelValue(doc, MARGIN, yb, "Location Address", rev, 18.0);
// const distKm = (caseLat != null && lat != null) ? haversineKm(caseLat, caseLng, lat, lng) : null;
// const distM = distKm ? Math.round(distKm * 1000) : null;
// doc.font("Helvetica").fontSize(10);
// doc.text(distM ? `Distance from case location: ${distM} meters` : "Distance from case location: N/A", MARGIN, yb);
// yb += 0.7 * CM;
// const upAt = fmtIST(f.uploaded_at);
// doc.text(`Uploaded At: ${upAt}`, MARGIN, yb); yb += 0.8 * CM;
// // Condensed metadata section
// doc.font("Helvetica-Bold").fontSize(11).text("Metadata", MARGIN, yb); yb += 0.5 * CM;
// doc.font("Helvetica").fontSize(9);
// doc.text(`Server Time: ${upAt}`, MARGIN, yb); yb += 0.4 * CM;
// if (lat != null) {
// doc.text(`Location: ${lat.toFixed(4)}, ${lng.toFixed(4)}`, MARGIN, yb);
// }
// yb += 0.4 * CM;
// drawFooter(doc, page);
// // Only add new page if not the last file
// if (idx < files.length - 1) {
// doc.addPage();
// page += 1;
// }
// }
// // Timeline page (optional - can be skipped for very large reports)
// if (files.length <= 10) { // Only add timeline for reasonable number of files
// console.log("Creating timeline page...");
// doc.addPage(); page += 1;
// drawHeader(doc, title);
// y = 3.4 * CM;
// doc.font("Helvetica-Bold").fontSize(14).text("Inspection Timeline", MARGIN, y); y += 0.8 * CM;
// doc.save();
// doc.roundedRect(MARGIN, y, A4_WIDTH - 2 * MARGIN, 1.1 * CM, 0.3 * CM).fillOpacity(1).fill("#E0F7E6");
// doc.fillColor("#1F743F").font("Helvetica-Bold").fontSize(12).text("Ready for Review", MARGIN + 0.6 * CM, y + 0.4 * CM);
// doc.fillColor("black");
// doc.restore();
// y += 2.0 * CM;
// const events = [];
// function evt(text, ts, sub = null) {
// if (ts) events.push({ text, ts, sub });
// }
// evt("Case Created", caseObj.created_at);
// evt("Assigned to Agent", caseObj.assigned_at);
// evt("Accepted by Agent", caseObj.accepted_at);
// // Limit timeline events
// for (let i = 0; i < Math.min(files.length, 5); i++) {
// const f = files[i];
// const sub = await reverseGeocode((f.geo_location || {}).lat, (f.geo_location || {}).lng);
// evt(`Upload — ${displayType(f.file_type)}`, f.uploaded_at, sub);
// }
// evt("Case Updated", caseObj.updated_at);
// evt("Case Completed", caseObj.completed_at);
// function parseTs(s) {
// try {
// return new Date(s);
// } catch {
// return new Date(0);
// }
// }
// events.sort((a, b) => parseTs(a.ts) - parseTs(b.ts));
// for (let ev of events) {
// doc.font("Helvetica").fontSize(9).text(`• ${fmtIST(ev.ts)}`, MARGIN, y); y += 0.4 * CM;
// doc.font("Helvetica-Bold").fontSize(10).text(ev.text, MARGIN + 0.6 * CM, y); y += 0.4 * CM;
// if (ev.sub) {
// doc.font("Helvetica").fontSize(8).fillColor("gray");
// const subText = ev.sub.length > 50 ? ev.sub.substring(0, 47) + "..." : ev.sub;
// doc.text(` ${subText}`, MARGIN + 0.6 * CM, y);
// doc.fillColor("black");
// y += 0.4 * CM;
// }
// y += 0.2 * CM;
// if (y > A4_HEIGHT - 3 * CM) {
// drawFooter(doc, page);
// doc.addPage(); page += 1;
// drawHeader(doc, title);
// y = 3.4 * CM;
// doc.font("Helvetica-Bold").fontSize(14).text("Inspection Timeline (Cont.)", MARGIN, y); y += 1.0 * CM;
// }
// }
// drawFooter(doc, page);
// }
// doc.end();
// ws.on("finish", () => {
// console.log(`Optimized PDF created successfully at ${outPath}`);
// resolve();
// });
// ws.on("error", (e) => {
// console.error("PDF write stream error:", e);
// reject(e);
// });
// } catch (e) {
// console.error("PDF build error:", e);
// reject(e);
// }
// });
// }
// // Helper functions
// function sectionTitle(doc, text, x, y) {
// doc.font("Helvetica-Bold").fontSize(14).text(text, x, y);
// return y + 0.9 * CM;
// }
// function labelValue(doc, x, y, label, value) {
// doc.font("Helvetica-Bold").fontSize(10).text(`${label}:`, x, y);
// doc.font("Helvetica").fontSize(10).text((value == null || value === "" || (Array.isArray(value) && value.length === 0)) ? "N/A" : value.toString(), x + 4.8 * CM, y);
// }
// // FastAPI-equivalent endpoints
// app.get("/health", (req, res) => {
// console.log("Health check");
// res.json({
// status: "healthy",
// env_loaded: !!ADMIN_EMAIL && !!ADMIN_PASSWORD && !!AES_KEY_STR && !!AES_IV_STR,
// pdf_compression: {
// enabled: PDF_COMPRESSION_ENABLED,
// image_quality: IMAGE_COMPRESSION_QUALITY,
// thumbnail_quality: THUMBNAIL_QUALITY,
// max_image_dimension: MAX_IMAGE_DIMENSION
// }
// });
// });
// app.post("/generate-report", async (req, res) => {
// console.log("Generate report request received");
// try {
// const caseId = (req.body || {}).case_id;
// if (!caseId) {
// console.error("Missing case_id in request");
// return res.status(400).json({ error: "case_id required" });
// }
// console.log(`Generating optimized report for case: ${caseId}`);
// await adminLogin();
// const caseObj = await getCase(caseId);
// const files = await getFiles(caseId);
// console.log(`Downloading and compressing ${files.length} files...`);
// // Download and immediately compress files
// for (let i = 0; i < files.length; i++) {
// console.log(`Processing file ${i+1}/${files.length}...`);
// const originalBuffer = await downloadFile(files[i].file_id);
// // Store compressed version
// files[i]._bin = await compressImageBuffer(originalBuffer, IMAGE_COMPRESSION_QUALITY);
// }
// // Create output directory
// const outDir = "/tmp/output";
// if (!fs.existsSync(outDir)) {
// console.log(`Creating output directory: ${outDir}`);
// fs.mkdirSync(outDir, { recursive: true });
// }
// // Create temporary files
// const timestamp = Date.now();
// const originalFilename = `Report_${caseId}_${timestamp}.pdf`;
// const optimizedFilename = `Report_${caseId}_${timestamp}_optimized.pdf`;
// const originalPath = path.join(outDir, originalFilename);
// const optimizedPath = path.join(outDir, optimizedFilename);
// console.log(`Building optimized PDF at: ${originalPath}`);
// await buildPdf(caseObj, files, originalPath);
// // Optimize the PDF
// console.log(`Optimizing PDF...`);
// await optimizePDF(originalPath, optimizedPath);
// // Read the optimized PDF
// const pdfContent = fs.readFileSync(optimizedPath);
// // Get file sizes for logging
// const originalStats = fs.existsSync(originalPath) ? fs.statSync(originalPath) : { size: 0 };
// const optimizedStats = fs.statSync(optimizedPath);
// const compressionRatio = originalStats.size > 0 ?
// ((originalStats.size - optimizedStats.size) / originalStats.size * 100).toFixed(1) : "N/A";
// console.log(`PDF optimization results:`);
// console.log(` Final size: ${Math.round(optimizedStats.size / 1024)}KB`);
// if (originalStats.size > 0) {
// console.log(` Reduction: ${compressionRatio}% (from ${Math.round(originalStats.size / 1024)}KB)`);
// }
// // Cleanup
// [originalPath, optimizedPath].forEach(filePath => {
// try {
// if (fs.existsSync(filePath)) {
// fs.unlinkSync(filePath);
// console.log(`Cleaned up: ${path.basename(filePath)}`);
// }
// } catch (e) {
// console.warn(`Could not delete ${filePath}:`, e.message);
// }
// });
// const outputFilename = `Report_${caseId}.pdf`;
// console.log(`Sending optimized PDF: ${outputFilename} (${Math.round(optimizedStats.size / 1024)}KB)`);
// res.setHeader("Content-Type", "application/pdf");
// res.setHeader("Content-Disposition", `attachment; filename="${outputFilename}"`);
// res.setHeader("X-PDF-Size", `${Math.round(optimizedStats.size / 1024)}KB`);
// res.setHeader("X-PDF-Compression", "Hugging Face Optimized");
// res.send(pdfContent);
// console.log("Optimized PDF sent successfully");
// } catch (e) {
// console.error("Error generating PDF:", e);
// res.status(500).json({
// error: `Error generating PDF: ${e.message || String(e)}`,
// stack: process.env.NODE_ENV === 'development' ? e.stack : undefined
// });
// }
// });
// app.get("/", (req, res) => {
// console.log("Root endpoint accessed");
// res.json({
// message: "Case Report Generator API (Hugging Face Optimized)",
// status: "running",
// pdf_compression: {
// enabled: PDF_COMPRESSION_ENABLED,
// image_quality: IMAGE_COMPRESSION_QUALITY,
// thumbnail_quality: THUMBNAIL_QUALITY,
// max_image_dimension: MAX_IMAGE_DIMENSION,
// note: "Optimized for Hugging Face Spaces - uses aggressive WebP compression"
// },
// endpoints: {
// health: "GET /health",
// generate: "POST /generate-report"
// }
// });
// });
// const PORT = process.env.PORT || 7860;
// app.listen(PORT, "0.0.0.0", () => {
// console.log(`Server listening on port ${PORT}`);
// console.log(`PDF Compression: ${PDF_COMPRESSION_ENABLED ? 'ENABLED' : 'DISABLED'}`);
// console.log(`Image Quality: ${IMAGE_COMPRESSION_QUALITY}%`);
// console.log(`Thumbnail Quality: ${THUMBNAIL_QUALITY}%`);
// console.log(`Max Image Dimension: ${MAX_IMAGE_DIMENSION}px`);
// console.log(`Optimized for Hugging Face Spaces`);
// });