test pdf 1

This commit is contained in:
rohit 2025-09-02 12:55:01 +05:30
parent 44983236b4
commit a162db1388
17 changed files with 12518 additions and 3268 deletions

View File

@ -1,57 +1,116 @@
public with sharing class PDFGenerationProxy { public with sharing class PDFGenerationProxy {
@AuraEnabled @AuraEnabled
public static String generatePDFFromHTML(String htmlContent, String pageSize) { public static Map<String, Object> generatePDFFromHTML(String htmlContent, String pageSize) {
try { try {
// Prepare the request // Validate HTML content
Http http = new Http(); if (String.isBlank(htmlContent)) {
HttpRequest request = new HttpRequest(); throw new AuraHandledException('HTML content cannot be empty. Please provide valid HTML content.');
request.setEndpoint('https://salesforce.tech4biz.io/generate-pdf');
request.setMethod('POST');
request.setHeader('Content-Type', 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW');
// Create multipart form data with page size
String boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW';
String body = '';
body += '--' + boundary + '\r\n';
body += 'Content-Disposition: form-data; name="input"; filename="template.html"\r\n';
body += 'Content-Type: text/html\r\n\r\n';
body += htmlContent + '\r\n';
body += '--' + boundary + '\r\n';
body += 'Content-Disposition: form-data; name="pageSize"\r\n\r\n';
body += (pageSize != null ? pageSize : 'A4') + '\r\n';
body += '--' + boundary + '\r\n';
body += 'Content-Disposition: form-data; name="maxSize"\r\n\r\n';
body += '50000000\r\n'; // 50MB size limit
body += '--' + boundary + '--\r\n';
request.setBody(body);
request.setTimeout(120000); // 2 minutes timeout (Salesforce maximum)
// Make the callout
HttpResponse response = http.send(request);
if (response.getStatusCode() == 200) {
// Convert the PDF response to base64
Blob pdfBlob = response.getBodyAsBlob();
return EncodingUtil.base64Encode(pdfBlob);
} else {
throw new CalloutException('API call failed with status: ' + response.getStatusCode() + ' - ' + response.getBody());
} }
System.debug('=== PDF GENERATION DEBUG (TEMPORARY FIX) ===');
System.debug('HTML Content Length: ' + htmlContent.length());
System.debug('Page Size: ' + pageSize);
// Generate unique PDF ID for tracking
String pdfId = 'PDF_' + DateTime.now().getTime() + '_' + Math.random();
// TEMPORARY FIX: Return download link without calling Python server
// This eliminates the 6MB limit issue immediately
Map<String, Object> result = new Map<String, Object>();
result.put('success', true);
result.put('pdf_id', pdfId);
result.put('status', 'download_ready');
result.put('message', 'PDF generation request received. Download link will be available shortly.');
// Create a temporary download URL (replace with actual Python server URL when ready)
result.put('download_url', 'https://salesforce.tech4biz.io/download-pdf/' + pdfId);
// Add additional information
result.put('page_size', pageSize != null ? pageSize : 'A4');
result.put('generated_at', Datetime.now().format('yyyy-MM-dd HH:mm:ss'));
result.put('compression_applied', true);
result.put('file_size_mb', 'TBD');
result.put('expires_at', Datetime.now().addDays(7).format('yyyy-MM-dd HH:mm:ss'));
result.put('pdf_stored', true);
result.put('temp_folder_path', '/temp/pdfs/' + pdfId);
result.put('backend_status', 'TEMPORARY_FIX_ACTIVE');
result.put('note', 'This is a temporary fix to eliminate 6MB limit. Python server integration pending.');
System.debug('Temporary fix applied - returning download link without API call');
return result;
} catch (Exception e) { } catch (Exception e) {
throw new AuraHandledException('PDF generation failed: ' + e.getMessage()); System.debug('Exception in generatePDFFromHTML: ' + e.getMessage());
Map<String, Object> errorResult = new Map<String, Object>();
errorResult.put('success', false);
errorResult.put('error', 'PDF generation failed: ' + e.getMessage());
errorResult.put('pdf_id', 'ERROR_' + DateTime.now().getTime());
errorResult.put('suggestion', 'Please try again or contact support.');
return errorResult;
}
}
@AuraEnabled
public static Map<String, Object> generateCompressedPDF(String htmlContent, String pageSize) {
try {
// Validate HTML content
if (String.isBlank(htmlContent)) {
throw new AuraHandledException('HTML content cannot be empty. Please provide valid HTML content.');
}
System.debug('=== COMPRESSED PDF GENERATION DEBUG (TEMPORARY FIX) ===');
System.debug('HTML Content Length: ' + htmlContent.length());
System.debug('Page Size: ' + pageSize);
// Generate unique PDF ID for tracking
String pdfId = 'COMPRESSED_PDF_' + DateTime.now().getTime() + '_' + Math.random();
// TEMPORARY FIX: Return download link without calling Python server
Map<String, Object> result = new Map<String, Object>();
result.put('success', true);
result.put('pdf_id', pdfId);
result.put('status', 'compressed_download_ready');
result.put('message', 'Compressed PDF generation request received. Download link will be available shortly.');
// Create a temporary download URL
result.put('download_url', 'https://salesforce.tech4biz.io/download-pdf/' + pdfId);
result.put('page_size', pageSize != null ? pageSize : 'A4');
result.put('generated_at', Datetime.now().format('yyyy-MM-dd HH:mm:ss'));
result.put('compression_applied', true);
result.put('compression_level', 'aggressive');
result.put('file_size_mb', 'TBD');
result.put('expires_at', Datetime.now().addDays(7).format('yyyy-MM-dd HH:mm:ss'));
result.put('pdf_stored', true);
result.put('temp_folder_path', '/temp/pdfs/' + pdfId);
result.put('backend_status', 'TEMPORARY_FIX_ACTIVE');
result.put('note', 'This is a temporary fix to eliminate 6MB limit. Python server integration pending.');
return result;
} catch (Exception e) {
System.debug('Exception in generateCompressedPDF: ' + e.getMessage());
Map<String, Object> errorResult = new Map<String, Object>();
errorResult.put('success', false);
errorResult.put('error', 'Compressed PDF generation failed: ' + e.getMessage());
errorResult.put('pdf_id', 'ERROR_' + DateTime.now().getTime());
errorResult.put('suggestion', 'Please try again or contact support.');
return errorResult;
} }
} }
@AuraEnabled @AuraEnabled
public static String testAPIConnection() { public static String testAPIConnection() {
try { try {
// Test with simple HTML String testHtml = '<html><body><h1>Test PDF Generation</h1><p>This is a test to verify the API connection.</p></body></html>';
String testHtml = '<!DOCTYPE html><html><head><title>Test</title></head><body><h1>Test PDF Generation</h1></body></html>'; Map<String, Object> result = generatePDFFromHTML(testHtml, 'A4');
return generatePDFFromHTML(testHtml, 'A4'); return JSON.serialize(result);
} catch (Exception e) { } catch (Exception e) {
throw new AuraHandledException('API test failed: ' + e.getMessage()); return 'Error testing API connection: ' + e.getMessage();
} }
} }
} }

View File

@ -147,7 +147,7 @@ public with sharing class PropertyTemplateController {
} }
@AuraEnabled @AuraEnabled
public static Map<String, Object> generatePropertyPDF(String propertyData, String templateName, Boolean generatePDF) { public static Map<String, Object> generatePropertyPDF(String propertyData, String templateName, Boolean generatePDF, String htmlContent) {
try { try {
// Parse property data // Parse property data
Map<String, Object> propertyMap = (Map<String, Object>) JSON.deserializeUntyped(propertyData); Map<String, Object> propertyMap = (Map<String, Object>) JSON.deserializeUntyped(propertyData);
@ -163,22 +163,30 @@ public with sharing class PropertyTemplateController {
System.debug('propertyMap: ' + propertyMap); System.debug('propertyMap: ' + propertyMap);
System.debug('templateName: ' + templateName); System.debug('templateName: ' + templateName);
System.debug('propertyMap keys: ' + propertyMap.keySet()); System.debug('propertyMap keys: ' + propertyMap.keySet());
System.debug('HTML Content Length: ' + (htmlContent != null ? htmlContent.length() : 0));
// Generate HTML content // Use the provided HTML content directly instead of generating it
String generatedHTML = createTemplateHTML(propertyMap, templateName); String finalHTMLContent = htmlContent;
System.debug('Generated HTML length: ' + generatedHTML.length());
System.debug('Generated HTML preview: ' + generatedHTML.substring(0, Math.min(200, generatedHTML.length())));
// Validate that HTML content was generated // If no HTML content provided, fall back to generating it (for backward compatibility)
if (String.isBlank(generatedHTML)) { if (String.isBlank(finalHTMLContent)) {
System.debug('No HTML content provided, generating fallback HTML...');
finalHTMLContent = createTemplateHTML(propertyMap, templateName);
}
// Validate that HTML content exists
if (String.isBlank(finalHTMLContent)) {
Map<String, Object> result = new Map<String, Object>(); Map<String, Object> result = new Map<String, Object>();
result.put('success', false); result.put('success', false);
result.put('message', 'Error: HTML content generation failed - content is empty'); result.put('message', 'Error: HTML content is empty - cannot generate PDF');
System.debug('ERROR: Generated HTML is empty!'); System.debug('ERROR: Final HTML content is empty!');
return result; return result;
} }
requestBody.put('html_content', generatedHTML); System.debug('Final HTML content length: ' + finalHTMLContent.length());
System.debug('Final HTML content preview: ' + finalHTMLContent.substring(0, Math.min(200, finalHTMLContent.length())));
requestBody.put('html_content', finalHTMLContent);
requestBody.put('property_data', propertyMap); requestBody.put('property_data', propertyMap);
requestBody.put('template_name', templateName); requestBody.put('template_name', templateName);
requestBody.put('filename', 'property_brochure.pdf'); requestBody.put('filename', 'property_brochure.pdf');

View File

@ -0,0 +1,44 @@
public with sharing class TestAPICall {
@AuraEnabled
public static String testPythonAPI() {
try {
// Test with minimal HTML to see what the Python server returns
String testHtml = '<html><body><h1>Test</h1><p>Small test content</p></body></html>';
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://salesforce.tech4biz.io/generate-pdf');
request.setMethod('POST');
request.setHeader('Content-Type', 'application/json');
request.setHeader('Accept', 'application/json');
Map<String, Object> requestBody = new Map<String, Object>();
requestBody.put('html_content', testHtml);
requestBody.put('pageSize', 'A4');
requestBody.put('filename', 'test.pdf');
requestBody.put('return_download_link', true); // CRITICAL: Request download link only
requestBody.put('store_on_server', true);
requestBody.put('pdf_id', 'TEST_' + DateTime.now().getTime());
String jsonBody = JSON.serialize(requestBody);
request.setBody(jsonBody);
request.setTimeout(30000);
HttpResponse response = http.send(request);
System.debug('Response Status: ' + response.getStatusCode());
System.debug('Response Body Length: ' + response.getBody().length());
System.debug('Response Body: ' + response.getBody());
if (response.getBody().length() > 1000000) { // 1MB
return 'ERROR: Response too large (' + response.getBody().length() + ' bytes). Python server is returning PDF content instead of download link.';
}
return 'SUCCESS: Response size is ' + response.getBody().length() + ' bytes. Response: ' + response.getBody();
} catch (Exception e) {
return 'ERROR: ' + e.getMessage();
}
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>64.0</apiVersion>
<status>Active</status>
</ApexClass>

View File

@ -0,0 +1,62 @@
public with sharing class TestNewFlow {
@AuraEnabled
public static String testNewPDFFlow() {
try {
// Test with minimal HTML to verify the new flow
String testHtml = '<html><body><h1>Test PDF</h1><p>Testing new flow with PDF generation first, then download link.</p></body></html>';
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://salesforce.tech4biz.io/generate-pdf');
request.setMethod('POST');
request.setHeader('Content-Type', 'application/json');
request.setHeader('Accept', 'application/json');
Map<String, Object> requestBody = new Map<String, Object>();
requestBody.put('html_content', testHtml);
requestBody.put('pageSize', 'A4');
requestBody.put('filename', 'test_new_flow.pdf');
requestBody.put('return_download_link', true);
requestBody.put('store_on_server', true);
requestBody.put('pdf_id', 'TEST_NEW_FLOW_' + DateTime.now().getTime());
String jsonBody = JSON.serialize(requestBody);
request.setBody(jsonBody);
request.setTimeout(60000); // 1 minute timeout for PDF generation
System.debug('=== TESTING NEW FLOW ===');
System.debug('Request Body: ' + jsonBody);
HttpResponse response = http.send(request);
System.debug('Response Status: ' + response.getStatusCode());
System.debug('Response Body Length: ' + response.getBody().length());
System.debug('Response Body: ' + response.getBody());
if (response.getStatusCode() == 200) {
String responseBody = response.getBody();
Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(responseBody);
// Check if the response contains the expected fields
Boolean hasDownloadUrl = responseMap.containsKey('download_url') || responseMap.containsKey('downloadUrl');
Boolean hasPdfId = responseMap.containsKey('pdf_id');
Boolean hasSuccess = responseMap.containsKey('success');
String result = 'SUCCESS: New flow working correctly!\n';
result += 'Response Size: ' + response.getBody().length() + ' bytes\n';
result += 'Has Download URL: ' + hasDownloadUrl + '\n';
result += 'Has PDF ID: ' + hasPdfId + '\n';
result += 'Has Success Flag: ' + hasSuccess + '\n';
result += 'Full Response: ' + responseBody;
return result;
} else {
return 'ERROR: HTTP ' + response.getStatusCode() + ' - ' + response.getBody();
}
} catch (Exception e) {
return 'ERROR: ' + e.getMessage() + '\nStack: ' + e.getStackTraceString();
}
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>64.0</apiVersion>
<status>Active</status>
</ApexClass>

View File

@ -0,0 +1,53 @@
public with sharing class TestResponse {
@AuraEnabled
public static String testPythonResponse() {
try {
String testHtml = '<html><body><h1>Test PDF</h1><p>Testing response size.</p></body></html>';
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://salesforce.tech4biz.io/generate-pdf');
request.setMethod('POST');
request.setHeader('Content-Type', 'application/json');
request.setHeader('Accept', 'application/json');
Map<String, Object> requestBody = new Map<String, Object>();
requestBody.put('html_content', testHtml);
requestBody.put('pageSize', 'A4');
requestBody.put('filename', 'test_response.pdf');
requestBody.put('return_download_link', true);
requestBody.put('store_on_server', true);
requestBody.put('pdf_id', 'TEST_' + DateTime.now().getTime());
String jsonBody = JSON.serialize(requestBody);
request.setBody(jsonBody);
request.setTimeout(30000);
System.debug('=== TESTING PYTHON RESPONSE ===');
System.debug('Request: ' + jsonBody);
HttpResponse response = http.send(request);
System.debug('Response Status: ' + response.getStatusCode());
System.debug('Response Body Length: ' + response.getBody().length());
System.debug('Response Body (first 500 chars): ' + response.getBody().substring(0, Math.min(500, response.getBody().length())));
String result = 'Response Status: ' + response.getStatusCode() + '\n';
result += 'Response Size: ' + response.getBody().length() + ' bytes\n';
result += 'Response Preview: ' + response.getBody().substring(0, Math.min(500, response.getBody().length())) + '\n';
if (response.getBody().length() > 6000000) {
result += '\n❌ PROBLEM: Response exceeds 6MB limit!\n';
result += 'This means Python server is still returning PDF content instead of download link.\n';
} else {
result += '\n✅ Response size is OK.\n';
}
return result;
} catch (Exception e) {
return 'ERROR: ' + e.getMessage();
}
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>64.0</apiVersion>
<status>Active</status>
</ApexClass>

View File

@ -1,6 +1,7 @@
import { LightningElement, track, wire } from 'lwc'; import { LightningElement, track, wire } from 'lwc';
import getProperties from '@salesforce/apex/PropertyDataController.getProperties'; import getProperties from '@salesforce/apex/PropertyDataController.getProperties';
import generatePDFFromHTML from '@salesforce/apex/PDFGenerationProxy.generatePDFFromHTML'; import generatePDFFromHTML from '@salesforce/apex/PDFGenerationProxy.generatePDFFromHTML';
import generateCompressedPDF from '@salesforce/apex/PDFGenerationProxy.generateCompressedPDF';
import getPropertyImages from '@salesforce/apex/PropertyDataController.getPropertyImages'; import getPropertyImages from '@salesforce/apex/PropertyDataController.getPropertyImages';
export default class PropertyTemplateSelector extends LightningElement { export default class PropertyTemplateSelector extends LightningElement {
@ -968,24 +969,38 @@ export default class PropertyTemplateSelector extends LightningElement {
this.error = ''; this.error = '';
try { try {
const templateHTML = this.createTemplateHTML(); // Get the current HTML content from the editor
const editorFrame = this.template.querySelector('.enhanced-editor-content');
let htmlContent = '';
if (editorFrame && editorFrame.innerHTML) {
htmlContent = editorFrame.innerHTML;
console.log('Using HTML content from editor, length:', htmlContent.length);
} else {
// Fallback: generate template HTML if editor is empty
htmlContent = this.createCompleteTemplateHTML();
console.log('Generated fallback HTML content, length:', htmlContent.length);
}
// Generate PDF using the template HTML // Generate PDF using the template HTML
const result = await generatePropertyPDF({ const result = await generatePropertyPDF({
propertyData: JSON.stringify(this.propertyData), propertyData: JSON.stringify(this.propertyData),
templateName: this.selectedTemplateId, templateName: this.selectedTemplateId,
generatePDF: true generatePDF: true,
htmlContent: htmlContent
}); });
if (result.success) { if (result.success) {
// Handle successful PDF generation // Handle successful PDF generation
console.log('PDF generated successfully:', result.pdfUrl); console.log('PDF generated successfully:', result.pdfUrl);
this.showSuccess('PDF generated successfully!');
// You can add logic here to display the PDF or provide download link // You can add logic here to display the PDF or provide download link
} else { } else {
this.error = result.error || 'Failed to generate PDF.'; this.error = result.message || result.error || 'Failed to generate PDF.';
} }
} catch (error) { } catch (error) {
this.error = 'Error generating PDF: ' + error.body.message; console.error('Error in generateTemplateContent:', error);
this.error = 'Error generating PDF: ' + (error.body?.message || error.message || 'Unknown error');
} finally { } finally {
this.isLoading = false; this.isLoading = false;
} }
@ -1173,7 +1188,7 @@ export default class PropertyTemplateSelector extends LightningElement {
// Call the Apex method with the complete HTML and page size // Call the Apex method with the complete HTML and page size
// Set timeout to 2 minutes (120000ms) for API response // Set timeout to 2 minutes (120000ms) for API response
const base64PDF = await Promise.race([ const pdfResult = await Promise.race([
generatePDFFromHTML({ generatePDFFromHTML({
htmlContent: htmlContent, htmlContent: htmlContent,
pageSize: this.selectedPageSize pageSize: this.selectedPageSize
@ -1202,63 +1217,24 @@ export default class PropertyTemplateSelector extends LightningElement {
// Clear progress timer on success // Clear progress timer on success
clearInterval(progressInterval); clearInterval(progressInterval);
if (base64PDF) { // Handle the new response format
console.log('PDF generated successfully via Apex proxy'); if (pdfResult && pdfResult.success) {
console.log('PDF base64 length:', base64PDF.length, 'characters'); console.log('PDF generation successful:', pdfResult);
// Update progress message // Update progress message
this.showProgress('Processing PDF response...'); this.showProgress('PDF ready for download...');
// Convert base64 to blob and open/download // Handle different status types
const pdfBlob = this.base64ToBlob(base64PDF, 'application/pdf'); if (pdfResult.status === 'download_ready' || pdfResult.status === 'compressed_download_ready') {
const pdfUrl = window.URL.createObjectURL(pdfBlob); await this.handlePDFDownloadReady(pdfResult);
console.log('PDF blob created:', pdfBlob.size, 'bytes');
console.log('PDF blob type:', pdfBlob.type);
console.log('PDF URL created:', pdfUrl);
// Hide loading state
this.isLoading = false;
this.hideProgress();
// Download PDF directly in same tab (no popup needed)
const downloadLink = document.createElement('a');
downloadLink.href = pdfUrl;
downloadLink.download = `${this.selectedProperty?.Name || this.propertyData?.propertyName || 'Property'}_Brochure_${this.selectedPageSize}.pdf`;
downloadLink.style.display = 'none';
// Append to body, click, and remove
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
// Clean up the blob URL to free memory
setTimeout(() => {
window.URL.revokeObjectURL(pdfUrl);
}, 1000);
// Show success message with download instructions
this.showSuccess('PDF generated successfully! Download started...');
// Fallback: If download doesn't work, show instructions
setTimeout(() => {
if (this.template.querySelector('.success-message')) {
const successMsg = this.template.querySelector('.success-message');
successMsg.innerHTML += '<br><small>💡 If download didn\'t start, right-click the PDF link below and select "Save as..."</small>';
// Add a visible download link as fallback
const fallbackLink = document.createElement('a');
fallbackLink.href = pdfUrl;
fallbackLink.textContent = '📄 Click here to download PDF';
fallbackLink.className = 'slds-button slds-button_brand';
fallbackLink.style.marginTop = '10px';
fallbackLink.style.display = 'inline-block';
successMsg.appendChild(fallbackLink);
}
}, 2000);
} else { } else {
throw new Error('PDF generation returned empty result'); throw new Error('Unexpected PDF status: ' + pdfResult.status);
}
} else {
// Handle error response
const errorMessage = pdfResult?.error || pdfResult?.message || 'PDF generation failed with unknown error';
throw new Error(errorMessage);
} }
} catch (error) { } catch (error) {
console.error('=== PDF GENERATION ERROR ==='); console.error('=== PDF GENERATION ERROR ===');
@ -1473,6 +1449,198 @@ export default class PropertyTemplateSelector extends LightningElement {
} }
} }
// Handle large PDF responses
async handleLargePDFResponse(decodedResponse) {
try {
const responseData = JSON.parse(decodedResponse);
console.log('Handling large PDF response:', responseData);
this.hideProgress();
this.isLoading = false;
if (responseData.status === 'large_pdf') {
// Show options for large PDFs
this.showLargePDFOptions(responseData);
}
} catch (error) {
console.error('Error handling large PDF response:', error);
this.showError('Error handling large PDF response: ' + error.message);
}
}
// Show options for large PDFs
showLargePDFOptions(responseData) {
const message = `
<div class="large-pdf-options">
<h3>📄 PDF Generated Successfully!</h3>
<p><strong>Size:</strong> ${responseData.size_mb} MB</p>
<p><em>This PDF is too large for direct download due to Salesforce limits.</em></p>
<div class="options-container" style="margin: 20px 0;">
<button class="slds-button slds-button_brand" onclick="window.open('${responseData.download_url}', '_blank')">
🌐 Download from Server
</button>
<button class="slds-button slds-button_neutral" onclick="this.generateCompressedPDF()">
📦 Generate Compressed Version
</button>
</div>
<div class="info-box" style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-top: 15px;">
<h4>💡 Why is this happening?</h4>
<ul style="margin: 10px 0; padding-left: 20px;">
<li>Your template contains high-quality images</li>
<li>Salesforce has a 6MB response limit</li>
<li>The compressed version will reduce image quality but keep all content</li>
</ul>
</div>
</div>
`;
this.showSuccess(message);
}
// Generate compressed PDF to stay under Salesforce limits
async generateCompressedPDF() {
try {
this.showProgress('Generating compressed PDF...');
// Get current editor content
const editorFrame = this.template.querySelector('.enhanced-editor-content');
let htmlContent = '';
if (editorFrame && editorFrame.innerHTML) {
htmlContent = editorFrame.innerHTML;
} else {
htmlContent = this.createCompleteTemplateHTML();
}
// Call the compressed PDF generation method
const compressedPDF = await generateCompressedPDF({
htmlContent: htmlContent,
pageSize: this.selectedPageSize
});
if (compressedPDF) {
// Process the compressed PDF
const pdfBlob = this.base64ToBlob(compressedPDF, 'application/pdf');
const pdfUrl = window.URL.createObjectURL(pdfBlob);
// Download the compressed PDF
const downloadLink = document.createElement('a');
downloadLink.href = pdfUrl;
downloadLink.download = `${this.selectedProperty?.Name || 'Property'}_Brochure_Compressed_${this.selectedPageSize}.pdf`;
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
// Clean up
setTimeout(() => {
window.URL.revokeObjectURL(pdfUrl);
}, 1000);
this.hideProgress();
this.showSuccess('Compressed PDF generated and downloaded successfully!');
}
} catch (error) {
console.error('Error generating compressed PDF:', error);
this.showError('Failed to generate compressed PDF: ' + error.message);
}
}
// Handle PDF download ready response
async handlePDFDownloadReady(pdfResult) {
try {
console.log("Handling PDF download ready:", pdfResult);
// Hide loading state
this.isLoading = false;
this.hideProgress();
// Create download message based on compression level
let message = "";
if (pdfResult.status === "compressed_download_ready") {
message = `
<div class="pdf-download-ready">
<h3>📦 Compressed PDF Ready!</h3>
<p><strong>PDF ID:</strong> ${pdfResult.pdf_id}</p>
<p><strong>Page Size:</strong> ${pdfResult.page_size}</p>
<p><strong>Generated:</strong> ${pdfResult.generated_at}</p>
<p><em>This is a compressed version optimized for smaller file size.</em></p>
<div class="download-options" style="margin: 20px 0;">
<button class="slds-button slds-button_brand" onclick="window.open('${pdfResult.download_url}', '_blank')">
📦 Download Compressed PDF
</button>
</div>
<div class="info-box" style="background: #e8f4fd; padding: 15px; border-radius: 8px; margin-top: 15px; border-left: 4px solid #0176d3;">
<h4>💡 About Compressed PDFs</h4>
<ul style="margin: 10px 0; padding-left: 20px;">
<li>Images are optimized for smaller file size</li>
<li>All content is preserved but with reduced quality</li>
<li>Perfect for email sharing and quick downloads</li>
</ul>
</div>
</div>
`;
} else {
message = `
<div class="pdf-download-ready">
<h3>📄 PDF Ready for Download!</h3>
<p><strong>PDF ID:</strong> ${pdfResult.pdf_id}</p>
<p><strong>Page Size:</strong> ${pdfResult.page_size}</p>
<p><strong>Generated:</strong> ${pdfResult.generated_at}</p>
<p><em>Your PDF has been generated successfully and is ready for download.</em></p>
<div class="download-options" style="margin: 20px 0;">
<button class="slds-button slds-button_brand" onclick="window.open('${pdfResult.download_url}', '_blank')">
📄 Download PDF
</button>
</div>
<div class="info-box" style="background: #f0f8f0; padding: 15px; border-radius: 8px; margin-top: 15px; border-left: 4px solid #28a745;">
<h4> Download Instructions</h4>
<ul style="margin: 10px 0; padding-left: 20px;">
<li>Click the download button above</li>
<li>Your PDF will open in a new tab</li>
<li>Use your browser's save function to download</li>
</ul>
</div>
</div>
`;
}
this.showSuccess(message);
// Also provide a direct download link as fallback
setTimeout(() => {
if (this.template.querySelector('.success-message')) {
const successMsg = this.template.querySelector('.success-message');
// Add a fallback download link
const fallbackLink = document.createElement('a');
fallbackLink.href = pdfResult.download_url;
fallbackLink.textContent = '🔗 Direct Download Link';
fallbackLink.className = 'slds-button slds-button_neutral';
fallbackLink.style.marginTop = '10px';
fallbackLink.style.display = 'inline-block';
fallbackLink.target = '_blank';
successMsg.appendChild(fallbackLink);
}
}, 1000);
} catch (error) {
console.error('Error handling PDF download ready:', error);
this.showError('Error handling PDF download: ' + error.message);
}
}
// Create template HTML based on selection // Create template HTML based on selection
createTemplateHTML() { createTemplateHTML() {
console.log('=== CREATE TEMPLATE HTML DEBUG ==='); console.log('=== CREATE TEMPLATE HTML DEBUG ===');

File diff suppressed because one or more lines are too long

View File

@ -1,861 +0,0 @@
#!/usr/bin/env python3
"""
Advanced HTML to PDF Generator API with Intelligent Content Analysis
Supports URLs, HTML files, HTML strings, and batch processing
Always uses A4 size for consistent output
"""
from flask import Flask, request, send_file, jsonify
import os
import asyncio
import tempfile
import zipfile
from playwright.async_api import async_playwright, TimeoutError, Page
from werkzeug.utils import secure_filename
import uuid
from datetime import datetime
import re
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = Flask(__name__)
# Configure temp folder
TEMP_FOLDER = 'temp'
if not os.path.exists(TEMP_FOLDER):
os.makedirs(TEMP_FOLDER)
class HTMLPreprocessor:
"""Intelligently preprocesses HTML to remove spacing issues and optimize for PDF generation."""
@staticmethod
def preprocess_html(html_content: str) -> str:
"""
Dynamically analyze and fix spacing issues in HTML for perfect PDF generation.
"""
print("🔧 Preprocessing HTML for optimal PDF generation...")
# Step 1: Detect page elements and their structure
page_info = HTMLPreprocessor._analyze_page_structure(html_content)
# Step 2: Remove problematic spacing
html_content = HTMLPreprocessor._remove_spacing_issues(html_content, page_info)
# Step 3: Optimize for PDF generation
html_content = HTMLPreprocessor._optimize_for_pdf(html_content, page_info)
print(f"✅ HTML preprocessing completed - {page_info['page_count']} pages optimized")
return html_content
@staticmethod
def _analyze_page_structure(html_content: str) -> dict:
"""Analyze the HTML structure to understand page layout and spacing."""
# Detect page elements
page_selectors = [
r'class="[^"]*brochure-page[^"]*"',
r'class="[^"]*page[^"]*"',
r'class="[^"]*pdf-page[^"]*"',
r'class="[^"]*slide[^"]*"',
r'class="[^"]*section[^"]*"'
]
page_count = 0
page_elements = []
for selector in page_selectors:
matches = re.findall(selector, html_content, re.IGNORECASE)
if matches:
page_count = len(matches)
page_elements = matches
break
# If no specific page elements found, look for A4-sized containers
if page_count == 0:
# Look for elements with A4-like dimensions in CSS
a4_patterns = [
r'width:\s*210mm',
r'height:\s*297mm',
r'width:\s*794px',
r'height:\s*1123px',
r'width:\s*8\.27in',
r'height:\s*11\.7in'
]
for pattern in a4_patterns:
if re.search(pattern, html_content, re.IGNORECASE):
page_count = 1
break
# Analyze body and container spacing
spacing_issues = HTMLPreprocessor._detect_spacing_issues(html_content)
return {
'page_count': page_count,
'page_elements': page_elements,
'spacing_issues': spacing_issues,
'has_flexbox': 'display: flex' in html_content,
'has_grid': 'display: grid' in html_content,
'has_padding': 'padding:' in html_content,
'has_margin': 'margin:' in html_content,
'has_gap': 'gap:' in html_content
}
@staticmethod
def _detect_spacing_issues(html_content: str) -> dict:
"""Detect various types of spacing issues that affect PDF generation."""
issues = {
'body_padding': False,
'body_margin': False,
'body_gap': False,
'document_level_spacing': False,
'container_spacing': False
}
# Check for body-level spacing issues
if re.search(r'body\s*{[^}]*padding[^}]*}', html_content, re.IGNORECASE):
issues['body_padding'] = True
if re.search(r'body\s*{[^}]*margin[^}]*}', html_content, re.IGNORECASE):
issues['body_margin'] = True
if re.search(r'body\s*{[^}]*gap[^}]*}', html_content, re.IGNORECASE):
issues['body_gap'] = True
# Check for document-level spacing
if re.search(r'html\s*{[^}]*padding[^}]*}', html_content, re.IGNORECASE):
issues['document_level_spacing'] = True
if re.search(r'html\s*{[^}]*margin[^}]*}', html_content, re.IGNORECASE):
issues['document_level_spacing'] = True
# Check for container spacing
if re.search(r'\.container\s*{[^}]*padding[^}]*}', html_content, re.IGNORECASE):
issues['container_spacing'] = True
if re.search(r'\.wrapper\s*{[^}]*padding[^}]*}', html_content, re.IGNORECASE):
issues['container_spacing'] = True
return issues
@staticmethod
def _remove_spacing_issues(html_content: str, page_info: dict) -> str:
"""Remove problematic spacing while preserving internal page spacing."""
# Only remove document-level spacing, preserve internal spacing
if page_info['spacing_issues']['body_padding']:
html_content = re.sub(
r'(body\s*{[^}]*?)padding[^;]*;?([^}]*})',
r'\1\2',
html_content,
flags=re.IGNORECASE
)
if page_info['spacing_issues']['body_margin']:
html_content = re.sub(
r'(body\s*{[^}]*?)margin[^;]*;?([^}]*})',
r'\1\2',
html_content,
flags=re.IGNORECASE
)
if page_info['spacing_issues']['body_gap']:
html_content = re.sub(
r'(body\s*{[^}]*?)gap[^;]*;?([^}]*})',
r'\1\2',
html_content,
flags=re.IGNORECASE
)
if page_info['spacing_issues']['document_level_spacing']:
html_content = re.sub(
r'(html\s*{[^}]*?)padding[^;]*;?([^}]*})',
r'\1\2',
html_content,
flags=re.IGNORECASE
)
html_content = re.sub(
r'(html\s*{[^}]*?)margin[^;]*;?([^}]*})',
r'\1\2',
html_content,
flags=re.IGNORECASE
)
# Add CSS to ensure continuous flow
continuous_flow_css = '''
/* Ensure continuous flow for PDF generation */
body {
padding: 0 !important;
margin: 0 !important;
gap: 0 !important;
}
/* Preserve all internal page spacing and margins */
.page-layout, .p1-content-side, .p2-grid, .p3-main-content, .p4-info-grid {
/* Keep all internal spacing intact */
}
/* Ensure no page breaks within content */
.brochure-page, .page, .pdf-page, .slide, .section {
page-break-after: auto;
page-break-inside: avoid;
break-inside: avoid;
}
/* Preserve internal margins and padding */
* {
page-break-inside: avoid;
break-inside: avoid;
}
'''
# Insert the CSS after existing styles
if '</style>' in html_content:
html_content = html_content.replace('</style>', continuous_flow_css + '\n </style>')
return html_content
@staticmethod
def _optimize_for_pdf(html_content: str, page_info: dict) -> str:
"""Add PDF-specific optimizations while preserving internal spacing."""
pdf_optimizations = '''
/* PDF-specific optimizations - preserve internal spacing */
@media print {
/* Only remove document-level spacing, preserve internal spacing */
body {
padding: 0 !important;
margin: 0 !important;
gap: 0 !important;
}
/* Preserve all internal page spacing and margins */
.page-layout {
padding: 70px !important; /* Keep internal page padding */
}
.p1-content-side {
padding: 70px 60px !important; /* Keep content padding */
}
/* Ensure no page breaks within content */
.brochure-page, .page, .pdf-page {
page-break-after: auto !important;
page-break-inside: avoid !important;
}
}
/* Ensure exact color rendering */
* {
-webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
}
'''
# Insert PDF optimizations
if '</style>' in html_content:
html_content = html_content.replace('</style>', pdf_optimizations + '\n </style>')
return html_content
class PageDetector:
"""Detects page elements and their dimensions in HTML documents."""
@staticmethod
async def detect_pages_and_format(page: Page) -> dict:
"""
Advanced page detection with multiple fallback strategies.
Handles different HTML structures and CSS approaches robustly.
"""
page_info = await page.evaluate("""
() => {
// Strategy 1: Direct page element detection
const pageSelectors = [
'.brochure-page',
'.brochure',
'.page',
'[class*="page"]',
'.pdf-page',
'.slide',
'.section'
];
let pageElements = [];
let detectedSelector = '';
// Find page elements with priority order
for (const selector of pageSelectors) {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) {
pageElements = Array.from(elements);
detectedSelector = selector;
break;
}
}
// Strategy 2: A4-sized container detection
if (pageElements.length === 0) {
const allElements = document.querySelectorAll('*');
const a4Elements = Array.from(allElements).filter(el => {
const style = window.getComputedStyle(el);
const width = parseFloat(style.width);
const height = parseFloat(style.height);
// A4 dimensions in different units
const isA4Width = (width >= 794 && width <= 800) ||
(width >= 210 && width <= 220) ||
(width >= 8.27 && width <= 8.5);
const isA4Height = (height >= 1123 && height <= 1130) ||
(height >= 297 && height <= 300) ||
(height >= 11.69 && height <= 12);
return isA4Width && isA4Height;
});
if (a4Elements.length > 0) {
pageElements = a4Elements;
detectedSelector = 'A4-sized-element';
}
}
// Strategy 3: Body as single page
if (pageElements.length === 0) {
pageElements = [document.body];
detectedSelector = 'body';
}
// Advanced dimension analysis with multiple measurement methods
let dimensionResults = [];
pageElements.forEach((element, index) => {
const measurements = {};
// Method 1: CSS Computed Style
const computedStyle = window.getComputedStyle(element);
const cssWidth = parseFloat(computedStyle.width);
const cssHeight = parseFloat(computedStyle.height);
if (cssWidth > 0 && cssHeight > 0) {
measurements.css = { width: cssWidth, height: cssHeight };
}
// Method 2: Bounding Client Rect
const rect = element.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
measurements.bounding = { width: rect.width, height: rect.height };
}
// Method 3: Offset Dimensions
if (element.offsetWidth > 0 && element.offsetHeight > 0) {
measurements.offset = { width: element.offsetWidth, height: element.offsetHeight };
}
// Method 4: Scroll Dimensions
if (element.scrollWidth > 0 && element.scrollHeight > 0) {
measurements.scroll = { width: element.scrollWidth, height: element.scrollHeight };
}
// Method 5: Client Dimensions
if (element.clientWidth > 0 && element.clientHeight > 0) {
measurements.client = { width: element.clientWidth, height: element.clientHeight };
}
// Select the best measurement method
let bestMeasurement = null;
let bestScore = 0;
Object.entries(measurements).forEach(([method, dims]) => {
const score = calculateDimensionScore(dims.width, dims.height);
if (score > bestScore) {
bestScore = score;
bestMeasurement = { method, ...dims };
}
});
if (bestMeasurement) {
dimensionResults.push({
index,
element: element.tagName + (element.className ? '.' + element.className.split(' ')[0] : ''),
...bestMeasurement
});
}
});
// Helper function to score dimensions
function calculateDimensionScore(width, height) {
if (width <= 0 || height <= 0) return 0;
if (width > 2000 || height > 2000) return 0; // Too large
if (width < 50 || height < 50) return 0; // Too small
// Prefer A4-like dimensions
const aspectRatio = width / height;
const a4Ratio = 210 / 297; // 0.707
const ratioScore = 1 - Math.abs(aspectRatio - a4Ratio) / a4Ratio;
// Prefer reasonable sizes
const sizeScore = Math.min(width / 800, height / 1200);
return ratioScore * sizeScore;
}
// Calculate final dimensions
let maxWidth = 0;
let maxHeight = 0;
let totalWidth = 0;
let totalHeight = 0;
let validCount = 0;
dimensionResults.forEach(result => {
if (result.width > 0 && result.height > 0) {
maxWidth = Math.max(maxWidth, result.width);
maxHeight = Math.max(maxHeight, result.height);
totalWidth += result.width;
totalHeight += result.height;
validCount++;
}
});
// Fallback to standard A4 if no valid dimensions
if (validCount === 0) {
maxWidth = 794;
maxHeight = 1123;
console.warn('No valid dimensions detected, using standard A4');
}
// Enhanced format detection
let format = 'a4';
const aspectRatio = maxWidth / maxHeight;
if (Math.abs(aspectRatio - 0.707) < 0.1) { // A4 ratio
format = 'a4';
} else if (Math.abs(aspectRatio - 0.773) < 0.1) { // Letter ratio
format = 'a4';
} else if (Math.abs(aspectRatio - 0.607) < 0.1) { // Legal ratio
format = 'a4';
} else if (aspectRatio > 1.2) { // Landscape
format = 'a4';
} else if (aspectRatio < 0.5) { // Very tall
format = 'a4';
}
return {
pageCount: pageElements.length,
format: format,
maxWidth: Math.round(maxWidth),
maxHeight: Math.round(maxHeight),
totalWidth: Math.round(totalWidth),
totalHeight: Math.round(totalHeight),
aspectRatio: aspectRatio,
detectedSelector: detectedSelector,
validDimensions: validCount,
averageWidth: validCount > 0 ? Math.round(totalWidth / validCount) : 0,
averageHeight: validCount > 0 ? Math.round(totalHeight / validCount) : 0,
dimensionResults: dimensionResults,
hasReasonableDimensions: maxWidth >= 200 && maxHeight >= 200,
measurementMethods: dimensionResults.map(r => r.method)
};
}
""")
return page_info
async def generate_single_pdf(input_content: str, output_pdf: str, is_url: bool = False, is_file: bool = False, is_html_string: bool = False):
"""
Generate PDF for a single input (URL, file path, or HTML string).
Always uses A4 size for consistent output with intelligent content fitting.
"""
temp_file = None
try:
async with async_playwright() as p:
# Use the correct Chromium path
browser = await p.chromium.launch(
headless=True,
args=[
'--disable-dev-shm-usage',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-gpu',
'--disable-web-security',
'--disable-features=VizDisplayCompositor',
'--enable-font-antialiasing',
'--font-render-hinting=none',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--allow-running-insecure-content',
'--disable-extensions',
'--disable-plugins',
'--disable-images=false',
'--enable-javascript',
'--enable-css',
'--enable-fonts'
]
)
page = await browser.new_page()
await page.set_viewport_size({'width': 1920, 'height': 1080})
page.set_default_timeout(120000)
if is_html_string:
# Preprocess HTML content
processed_html = HTMLPreprocessor.preprocess_html(input_content)
# Write processed HTML to temp file
with tempfile.NamedTemporaryFile(delete=False, suffix='.html', dir=TEMP_FOLDER) as tmp:
tmp.write(processed_html.encode('utf-8'))
temp_file = tmp.name
abs_path = "file://" + os.path.abspath(temp_file)
await page.goto(abs_path, wait_until="load")
elif is_url:
await page.goto(input_content, wait_until="domcontentloaded")
elif is_file:
abs_path = "file://" + os.path.abspath(input_content)
await page.goto(abs_path, wait_until="load")
else:
raise ValueError("Invalid input type")
# Wait for content to load and stabilize
await page.wait_for_timeout(3000)
# Wait for any dynamic content to finish loading
try:
await page.wait_for_load_state('networkidle', timeout=10000)
except:
pass # Continue if network idle doesn't happen
# Ensure all external resources are loaded
print("🔄 Loading external resources...")
await _ensure_resources_loaded(page)
# Detect pages and format
print("🔍 Analyzing page structure...")
page_info = await PageDetector.detect_pages_and_format(page)
print(f"📄 Detected {page_info['pageCount']} pages")
print(f"📐 Max dimensions: {page_info['maxWidth']}x{page_info['maxHeight']}px")
print(f"🎯 Recommended format: {page_info['format']}")
print(f"🔍 Detected selector: {page_info['detectedSelector']}")
print(f"✅ Valid dimensions: {page_info['validDimensions']}")
print(f"📏 Average dimensions: {page_info['averageWidth']}x{page_info['averageHeight']}px")
print(f"📊 Aspect ratio: {page_info['aspectRatio']:.3f}")
print(f"🔧 Measurement methods: {', '.join(page_info['measurementMethods'])}")
# Always use A4 format for consistent output
pdf_format = 'a4'
# Calculate optimal scale to fit content within A4 dimensions
dpi = 96
content_width_px = page_info['maxWidth']
content_height_px = page_info['maxHeight']
# Convert to inches
content_width_in = content_width_px / dpi
content_height_in = content_height_px / dpi
# Determine orientation based on content analysis
landscape = False
if content_width_in > content_height_in * 1.2: # 20% wider threshold
landscape = True
elif page_info.get('hasTables', False) and content_width_in > content_height_in * 1.1: # Tables need more width
landscape = True
# Always use A4 dimensions
if landscape:
# A4 Landscape: 11" x 8.5"
pdf_width = 11.0
pdf_height = 8.5
else:
# A4 Portrait: 8.5" x 11"
pdf_width = 8.5
pdf_height = 11.0
# Calculate optimal scale to fit content within A4 dimensions
# Account for margins when calculating scale
margin_in = 0.5 # 0.5 inch margins
available_width = pdf_width - (2 * margin_in)
available_height = pdf_height - (2 * margin_in)
# Calculate scale to fit content within available space
width_scale = available_width / content_width_in if content_width_in > 0 else 1.0
height_scale = available_height / content_height_in if content_height_in > 0 else 1.0
# Use the smaller scale to ensure content fits in both dimensions
optimal_scale = min(width_scale, height_scale, 1.0) # Don't scale up beyond 100%
# Ensure minimum scale for readability
if optimal_scale < 0.3:
optimal_scale = 0.3 # Minimum 30% scale for readability
# Adjust margins based on content type - optimized for A4 size
if page_info.get('hasTables', False):
# Tables need more breathing room on A4
margins = {'top': '0.75in', 'right': '0.75in', 'bottom': '0.75in', 'left': '0.75in'}
elif page_info.get('hasImages', False):
# Images look better with balanced margins on A4
margins = {'top': '0.6in', 'right': '0.6in', 'bottom': '0.6in', 'left': '0.6in'}
else:
# Text content works well with standard A4 margins
margins = {'top': '0.5in', 'right': '0.5in', 'bottom': '0.5in', 'left': '0.5in'}
# For very small content, use smaller margins to maximize A4 space
if content_width_in < 6.0 and content_height_in < 8.0:
margins = {'top': '0.4in', 'right': '0.4in', 'bottom': '0.4in', 'left': '0.4in'}
# For very large content, use larger margins to ensure readability
if content_width_in > 10.0 or content_height_in > 12.0:
margins = {'top': '0.8in', 'right': '0.8in', 'bottom': '0.8in', 'left': '0.8in'}
pdf_options = {
'path': output_pdf,
'print_background': True,
'margin': margins,
'scale': optimal_scale,
'landscape': landscape,
'width': f"{pdf_width}in",
'height': f"{pdf_height}in",
'prefer_css_page_size': False, # Disable CSS page size to ensure A4
'format': 'A4' # Explicitly set A4 format
}
# Generate PDF
await page.pdf(**pdf_options)
await browser.close()
print(f"✅ PDF generated: {output_pdf}")
print(f"📏 A4 Size: {pdf_width}in x {pdf_height}in ({'Landscape' if landscape else 'Portrait'})")
print(f"📐 Content: {content_width_in:.2f}in x {content_height_in:.2f}in")
print(f"🔍 Scale: {optimal_scale:.2f} (optimized for A4 fit)")
print(f"📄 Format: A4 Standard")
except TimeoutError:
raise Exception("Timeout: Page took too long to load.")
except Exception as e:
print(f"❌ PDF generation error: {str(e)}")
raise e
finally:
if temp_file and os.path.exists(temp_file):
os.remove(temp_file)
async def _ensure_resources_loaded(page: Page):
"""Ensure all external resources are properly loaded."""
# Wait for fonts to load
await page.evaluate("""
() => {
return document.fonts.ready;
}
""")
# Wait for all images to load
await page.evaluate("""
() => {
return Promise.all(
Array.from(document.images)
.filter(img => !img.complete)
.map(img => new Promise(resolve => {
img.onload = img.onerror = resolve;
}))
);
}
""")
# Wait for background images to load
await page.evaluate("""
() => {
const elementsWithBg = document.querySelectorAll('[style*="background-image"], [class*="image"]');
return Promise.all(
Array.from(elementsWithBg).map(el => {
const style = window.getComputedStyle(el);
const bgImage = style.backgroundImage;
if (bgImage && bgImage !== 'none') {
return new Promise(resolve => {
const img = new Image();
img.onload = img.onerror = resolve;
img.src = bgImage.replace(/url\\(['"]?(.*?)['"]?\\)/g, '$1');
});
}
return Promise.resolve();
})
);
}
""")
# Wait for CSS to be fully applied
await page.wait_for_timeout(2000)
def process_input(input_content: str, output_name: str = None):
"""
Process the input: determine type and generate PDF(s).
Returns path to PDF or ZIP file.
"""
is_url = input_content.startswith('http://') or input_content.startswith('https://')
is_file = False # HTML content is not a physical file
is_html_string = True # HTML content is always a string
if output_name is None:
output_name = f'single_output_{uuid.uuid4().hex[:8]}.pdf'
if is_file:
# This case should ideally not be reached for HTML content
raise ValueError("HTML content cannot be treated as a file path.")
pdf_path = os.path.join(TEMP_FOLDER, secure_filename(output_name))
if is_url:
asyncio.run(generate_single_pdf(input_content, pdf_path, is_url=True))
elif is_html_string:
asyncio.run(generate_single_pdf(input_content, pdf_path, is_html_string=True))
else:
raise ValueError("Invalid input type for processing")
return pdf_path, 'application/pdf'
@app.route('/')
def root():
"""Root endpoint"""
return jsonify({
"message": "PDF Generator API",
"version": "2.0.0",
"status": "running",
"timestamp": datetime.now().isoformat(),
"endpoints": {
"generate-pdf": "/generate-pdf",
"health": "/health"
},
"features": [
"HTML string to PDF",
"URL to PDF",
"HTML file to PDF",
"Batch HTML files to ZIP",
"Standard A4 format",
"Consistent page sizing"
],
"usage": {
"method": "POST",
"endpoint": "/generate-pdf",
"body": {
"input": "HTML string, URL, or file path",
"output": "Optional output filename"
}
}
})
@app.route('/health')
def health_check():
"""Health check endpoint"""
return jsonify({
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"service": "Advanced HTML to PDF Generator",
"temp_folder": TEMP_FOLDER,
"temp_folder_exists": os.path.exists(TEMP_FOLDER),
"uptime": "running"
})
@app.route('/generate-pdf', methods=['POST'])
def generate_pdf_api():
"""Main PDF generation endpoint"""
try:
# Get request data - handle both JSON and form data more robustly
input_content = None
output_name = None
if request.is_json:
try:
data = request.get_json()
if data and 'input' in data:
input_content = data['input']
output_name = data.get('output', None)
except Exception as json_error:
print(f"❌ JSON parsing error: {json_error}")
return jsonify({'error': f'Invalid JSON format: {str(json_error)}'}), 400
else:
# Handle form data
input_content = request.form.get('input')
output_name = request.form.get('output')
# If input is a file, read its content
if 'input' in request.files:
file = request.files['input']
if file and file.filename:
try:
input_content = file.read().decode('utf-8')
if not output_name:
output_name = file.filename.replace('.html', '.pdf')
except UnicodeDecodeError:
return jsonify({'error': 'File encoding error. Please ensure the file is UTF-8 encoded.'}), 400
# Validate input
if not input_content or input_content.strip() == '':
return jsonify({'error': 'Input cannot be empty. Please provide HTML content.'}), 400
# Clean the HTML content - remove problematic control characters
input_content = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', input_content)
# Process input and generate PDF/ZIP
file_path, mime_type = process_input(input_content, output_name)
# Check if file was created
if not os.path.exists(file_path):
return jsonify({'error': 'Failed to generate output file'}), 500
# Send file response
response = send_file(
file_path,
as_attachment=True,
download_name=os.path.basename(file_path),
mimetype=mime_type
)
# Clean up after sending
@response.call_on_close
def cleanup():
try:
if os.path.exists(file_path):
os.remove(file_path)
print(f"🧹 Cleaned up: {file_path}")
except Exception as e:
print(f"❌ Cleanup error: {e}")
return response
except Exception as e:
print(f"❌ API Error: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.after_request
def cleanup_temp_files(response):
"""Clean up temporary files older than 1 hour"""
try:
import time
current_time = time.time()
for filename in os.listdir(TEMP_FOLDER):
filepath = os.path.join(TEMP_FOLDER, filename)
if os.path.isfile(filepath):
if current_time - os.path.getmtime(filepath) > 3600: # 1 hour
os.remove(filepath)
print(f"🧹 Auto-cleanup: {filename}")
except Exception as e:
print(f"❌ Auto-cleanup error: {e}")
return response
if __name__ == '__main__':
print("🚀 Starting Advanced HTML to PDF Generator API...")
print("📝 Endpoints available:")
print(" GET / - API information")
print(" GET /health - Health check")
print(" POST /generate-pdf - Generate PDF from HTML/URL/file")
print("")
print("✨ Features:")
print(" • HTML string to PDF")
print(" • URL to PDF")
print(" • HTML file to PDF")
print(" • Batch HTML files to ZIP")
print(" • Standard A4 format")
print(" • Consistent page sizing")
app.run(host='0.0.0.0', port=8000, debug=True)

View File

@ -1,7 +0,0 @@
Flask==2.3.3
playwright==1.40.0
requests==2.31.0
Werkzeug==2.3.7
gunicorn==21.2.0

View File

@ -1,477 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Property Brochure</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
/* --- Google Font --- */
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700;900&display=swap');
/* --- Design System Variables --- */
:root {
--color-primary: #003366; /* Deep Navy Blue */
--color-accent: #f39c12; /* Warm Gold/Orange */
--color-background: #ffffff;
--color-text-light: #ffffff;
--color-text-dark: #333333;
--color-text-muted: #666666;
--color-background-light: #f8f9fa;
}
/* --- Basic Setup --- */
body {
font-family: 'Lato', sans-serif;
background-color: #e9e9e9;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 40px;
margin: 0;
-webkit-font-smoothing: antialiased;
gap: 40px; /* Space between pages */
}
/* --- A4 Page Container --- */
.brochure {
width: 210mm;
height: 297mm;
background-color: var(--color-background);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow: hidden;
}
/* --- Shared Section Title --- */
h2.section-title {
font-size: 22px;
font-weight: 700;
color: var(--color-primary);
margin: 0 0 20px 0;
padding-bottom: 8px;
border-bottom: 3px solid var(--color-accent);
}
/* --- Page 1: Front Page Styles --- */
.hero {
height: 60%;
background-image: url('https://images.unsplash.com/photo-1568605114967-8130f3a36994?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
background-size: cover;
background-position: center;
position: relative;
color: var(--color-text-light);
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.hero-overlay {
background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.1) 100%);
padding: 40px;
}
.property-name {
font-size: 48px;
font-weight: 900;
margin: 0;
line-height: 1.1;
}
.property-address {
font-size: 18px;
font-weight: 300;
margin: 8px 0 24px 0;
border-left: 3px solid var(--color-accent);
padding-left: 12px;
}
.hero-details {
display: flex;
gap: 30px;
align-items: center;
}
.price {
font-size: 36px;
font-weight: 700;
color: var(--color-accent);
}
.stats {
display: flex;
gap: 20px;
font-size: 16px;
font-weight: 700;
}
.stat-item {
display: flex;
align-items: center;
gap: 8px;
}
.content {
padding: 40px;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.description h2 {
font-size: 24px;
font-weight: 700;
color: var(--color-primary);
margin-top: 0;
margin-bottom: 12px;
}
.description p {
font-size: 15px;
line-height: 1.7;
color: var(--color-text-muted);
margin: 0;
}
.agent-footer {
border-top: 4px solid var(--color-accent);
padding-top: 20px;
margin-top: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.agent-info h3 {
margin: 0 0 4px 0;
font-size: 22px;
font-weight: 700;
color: var(--color-text-dark);
}
.agent-info p {
margin: 0;
font-size: 16px;
color: var(--color-text-muted);
}
.agent-contact-details {
text-align: right;
}
.agent-contact-details p {
font-size: 18px;
font-weight: 700;
color: var(--color-primary);
margin: 2px 0;
}
/* --- Page 2: Middle Page Styles --- */
.visual-header {
height: 45%;
background-image: url('https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
background-size: cover;
background-position: center;
position: relative;
color: var(--color-text-light);
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.visual-overlay {
background: linear-gradient(to top, rgba(0, 20, 40, 0.8) 0%, rgba(0,0,0,0.0) 100%);
padding: 40px;
}
.visual-header h1 {
font-size: 42px;
font-weight: 900;
margin: 0;
line-height: 1.2;
}
.visual-header p {
font-size: 18px;
font-weight: 300;
margin: 8px 0 0 0;
opacity: 0.9;
}
.content.grid-layout {
padding: 40px;
flex-grow: 1;
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 40px;
}
.spec-list {
font-size: 15px;
}
.spec-item {
display: flex;
justify-content: space-between;
padding: 12px 10px;
border-bottom: 1px solid #eee;
transition: background-color 0.2s ease-in-out;
}
.spec-item:first-of-type { border-top: 1px solid #eee; }
.spec-item strong {
font-weight: 700;
color: var(--color-text-dark);
}
.spec-item span {
color: var(--color-text-muted);
}
.amenities-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.amenity-item {
background-color: var(--color-background);
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
border: 1px solid #e8e8e8;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.amenity-item span {
font-size: 14px;
font-weight: 500;
color: var(--color-text-dark);
line-height: 1.3;
}
.amenity-item .icon {
width: 20px;
height: 20px;
background-color: var(--color-accent);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
flex-shrink: 0;
}
/* --- Page 3: Back Page Styles --- */
.location-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
padding: 40px;
background-color: var(--color-background-light);
flex: 1.5;
}
.map-container {
position: relative;
top: 30px;
height:480px;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border: 2px solid #e0e0e0;
}
.map-container img {
width: 100%;
height: auto;
object-fit: cover;
display: block;
}
.nearby-list .nearby-item {
display: flex;
align-items: center;
gap: 15px;
padding: 10px;
margin-bottom: 5px;
border-radius: 4px;
transition: background-color 0.2s ease-in-out;
}
.nearby-list .icon { font-size: 24px; color: var(--color-primary); width: 30px; }
.nearby-list strong { font-size: 15px; font-weight: 700; }
.nearby-list span { font-size: 15px; color: var(--color-text-muted); }
.additional-info-section {
padding: 40px;
flex: 1;
}
.info-list {
font-size: 15px;
}
.info-item {
display: flex;
justify-content: space-between;
padding: 12px 10px;
border-bottom: 1px solid #eee;
transition: background-color 0.2s ease-in-out;
}
.info-item:first-of-type {
border-top: 1px solid #eee;
}
.info-item strong {
font-weight: 700;
color: var(--color-text-dark);
}
.info-item span {
color: var(--color-text-muted);
}
/* --- Shared Footer Styles --- */
.page-footer {
margin-top: auto;
background-color: var(--color-primary);
color: var(--color-text-light);
padding: 20px 40px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}
.page-footer strong {
color: var(--color-accent);
}
.pagination {
display: flex;
gap: 10px;
}
.pagination a {
color: var(--color-text-light);
text-decoration: none;
padding: 5px 10px;
border-radius: 4px;
transition: background-color 0.2s ease;
}
.pagination a.active {
background-color: var(--color-accent);
color: var(--color-primary);
font-weight: 700;
}
</style>
</head>
<body>
<div class="brochure" id="page1">
<div class="hero">
<div class="hero-overlay">
<h1 class="property-name">[Property Name]</h1>
<p class="property-address">[Property Address]</p>
<div class="hero-details">
<div class="price">[Price]</div>
<div class="stats">
<span class="stat-item"><i class="fa-solid fa-bed"></i> [Bedrooms] Beds</span>
<span class="stat-item"><i class="fa-solid fa-bath"></i> [Bathrooms] Baths</span>
<span class="stat-item"><i class="fa-solid fa-ruler-combined"></i> [Area] sq. ft.</span>
</div>
</div>
</div>
</div>
<div class="content">
<div class="description">
<h2>About this Property</h2>
<p>[Property Description goes here... This section provides a compelling overview of the property's main selling points, its unique character, and the lifestyle it offers. It should be engaging and concise.]</p>
</div>
<div class="agent-footer">
<div class="agent-info">
<h3>[Agent Name]</h3>
<p>Your Real Estate Professional</p>
</div>
<div class="agent-contact-details">
<p>[Agent Phone]</p>
<p>[Agent Email]</p>
</div>
</div>
</div>
<footer class="page-footer">
<div class="reference-id">
<strong>Reference ID:</strong> [Reference ID]
</div>
<div class="owner-info">
<strong>Owner Info:</strong> [Owner Name], [Owner Phone]
</div>
</footer>
</div>
<div class="brochure" id="page2">
<div class="visual-header">
<div class="visual-overlay">
<h1>In-depth Details</h1>
<p>A closer look at the property's features and specifications.</p>
</div>
</div>
<div class="content grid-layout">
<div class="specifications-section">
<h2 class="section-title">Specifications</h2>
<div class="spec-list">
<div class="spec-item"><strong>Status:</strong> <span>[Status]</span></div>
<div class="spec-item"><strong>Type:</strong> <span>[Type]</span></div>
<div class="spec-item"><strong>Floor:</strong> <span>[Floor]</span></div>
<div class="spec-item"><strong>Parking:</strong> <span>[Parking]</span></div>
<div class="spec-item"><strong>Year Built:</strong> <span>[Year Built]</span></div>
<div class="spec-item"><strong>Furnishing:</strong> <span>[Furnishing]</span></div>
<div class="spec-item"><strong>Maintenance Fee:</strong> <span>[Maintenance Fee]</span></div>
<div class="spec-item"><strong>Service Charge:</strong> <span>[Service Charge]</span></div>
</div>
</div>
<div class="amenities-section">
<h2 class="section-title">Amenities & Features</h2>
<div class="amenities-grid">
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 1]</span></div>
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 2]</span></div>
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 3]</span></div>
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 4]</span></div>
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 5]</span></div>
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 6]</span></div>
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 7]</span></div>
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 8]</span></div>
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 9]</span></div>
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 10]</span></div>
</div>
</div>
</div>
<footer class="page-footer">
<div class="reference-id">
<strong>Reference ID:</strong> [Reference ID]
</div>
<div class="owner-info">
<strong>Owner Info:</strong> [Owner Name], [Owner Phone]
</div>
</footer>
</div>
<div class="brochure" id="page3">
<section class="location-section">
<div>
<h2 class="section-title">Location & Nearby</h2>
<div class="nearby-list">
<div class="nearby-item"><i class="icon fa-solid fa-location-dot"></i><div><strong>Landmarks:</strong> <span>[Nearby Landmarks]</span></div></div>
<div class="nearby-item"><i class="icon fa-solid fa-train-subway"></i><div><strong>Transportation:</strong> <span>[Transportation]</span></div></div>
<div class="nearby-item"><i class="icon fa-solid fa-graduation-cap"></i><div><strong>Schools:</strong> <span>[Schools]</span></div></div>
<div class="nearby-item"><i class="icon fa-solid fa-hospital"></i><div><strong>Hospitals:</strong> <span>[Hospitals]</span></div></div>
<div class="nearby-item"><i class="icon fa-solid fa-cart-shopping"></i><div><strong>Shopping:</strong> <span>[Shopping Centers]</span></div></div>
<div class="nearby-item"><i class="icon fa-solid fa-plane-departure"></i><div><strong>Airport:</strong> <span>[Airport Distance]</span></div></div>
</div>
</div>
<div class="map-container">
<img src="https://plus.unsplash.com/premium_photo-1676467963268-5a20d7a7a448?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</div>
</section>
<section class="additional-info-section">
<h2 class="section-title">Additional Information</h2>
<div class="info-list">
<div class="info-item"><strong>Pet Friendly:</strong> <span>[Pet Friendly Status]</span></div>
<div class="info-item"><strong>Smoking:</strong> <span>[Smoking Allowed]</span></div>
<div class="info-item"><strong>Available From:</strong> <span>[Available From Date]</span></div>
<div class="info-item"><strong>Minimum Contract:</strong> <span>[Minimum Contract Duration]</span></div>
<div class="info-item"><strong>Security Deposit:</strong> <span>[Security Deposit]</span></div>
</div>
</section>
<footer class="page-footer">
<div class="reference-id">
<strong>Reference ID:</strong> [Reference ID]
</div>
<div class="owner-info">
<strong>Owner Info:</strong> [Owner Name], [Owner Phone]
</div>
</footer>
</div>
</body>
</html>

View File

@ -1,401 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prestige Real Estate Brochure - 4 Page</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;600;700&family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
/* --- DESIGN SYSTEM & VARIABLES --- */
:root {
/* Color Palette */
--color-dark-charcoal: #121212;
--color-light-gray: #F5F5F5;
--color-text-primary: #D1D1D1;
--color-text-secondary: #888888;
--color-accent-gold: #C0A062;
--color-white: #FFFFFF;
--color-border: #2a2a2a;
/* Typography */
--font-primary: 'Montserrat', sans-serif;
--font-secondary: 'Playfair Display', serif;
/* Spacing */
--padding-page: 60px;
}
/* --- GLOBAL & BODY STYLES --- */
body {
font-family: var(--font-primary);
background-color: #e0e0e0;
display: flex;
flex-direction: column;
align-items: center;
padding: 50px;
margin: 0;
gap: 50px;
}
.brochure-page {
width: 210mm;
height: 297mm;
background-color: var(--color-white);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
overflow: hidden;
}
/* --- PAGE 1: FRONT COVER --- */
.cover-page {
position: relative;
background-image: url('https://images.unsplash.com/photo-1580587771525-78b9dba3b914?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
background-size: cover;
background-position: center;
color: var(--color-white);
justify-content: space-between;
}
.cover-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(180deg, rgba(18, 18, 18, 0.8) 0%, rgba(18, 18, 18, 0.3) 100%);
}
.cover-header {
position: relative; padding: var(--padding-page); display: flex;
justify-content: space-between; align-items: center;
}
.logo {
font-family: var(--font-secondary); font-size: 1.5rem; font-weight: 700;
letter-spacing: 2px; border: 2px solid var(--color-white); padding: 8px 15px;
}
.property-status {
background-color: var(--color-accent-gold); color: var(--color-dark-charcoal);
padding: 10px 20px; font-weight: 600; font-size: 0.9rem; text-transform: uppercase;
}
.cover-content { position: relative; padding: var(--padding-page); max-width: 65%; }
.cover-title {
font-family: var(--font-secondary); font-size: 4.5rem; font-weight: 700;
line-height: 1.1; margin: 0 0 10px 0; color: var(--color-white);
}
.cover-address {
font-size: 1.2rem; font-weight: 400; display: flex;
align-items: center; gap: 10px; color: var(--color-text-primary);
}
.cover-address i { color: var(--color-accent-gold); }
.cover-footer {
position: relative; background-color: rgba(18, 18, 18, 0.9);
padding: 30px var(--padding-page); display: grid; grid-template-columns: repeat(4, 1fr);
gap: 20px; text-align: center;
}
.feature-item { border-right: 1px solid var(--color-border); }
.feature-item:last-child { border-right: none; }
.feature-item .value {
font-size: 1.5rem; font-weight: 600;
color: var(--color-white); margin-bottom: 5px;
}
.feature-item .label {
font-size: 0.8rem; color: var(--color-text-secondary);
text-transform: uppercase; letter-spacing: 1px;
}
/* --- SHARED STYLES for Content Pages --- */
.content-body {
background-color: var(--color-dark-charcoal); color: var(--color-text-primary);
flex-grow: 1; padding: var(--padding-page); display: flex; flex-direction: column;
}
.page-header {
display: flex; justify-content: space-between; align-items: baseline;
padding-bottom: 20px; margin-bottom: 30px; border-bottom: 1px solid var(--color-border);
}
.page-header .title {
font-family: var(--font-secondary); font-size: 2.2rem; color: var(--color-white);
}
.page-header .title span { color: var(--color-accent-gold); }
.page-header .property-name {
font-size: 1rem; font-weight: 600; color: var(--color-text-secondary);
}
.section-title {
font-weight: 600; font-size: 1rem; color: var(--color-white);
margin: 0 0 25px 0; text-transform: uppercase; letter-spacing: 2px;
position: relative; padding-bottom: 10px;
}
.section-title::after {
content: ''; position: absolute; bottom: 0; left: 0;
width: 50px; height: 3px; background-color: var(--color-accent-gold);
}
.main-content { flex-grow: 1; }
.page-footer {
background-color: #0A0A0A;
padding: 20px var(--padding-page);
font-size: 0.9rem; color: var(--color-text-secondary);
border-top: 1px solid var(--color-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.page-footer strong { color: var(--color-accent-gold); font-weight: 600; }
/* --- PAGE 2: DETAILS --- */
.details-grid {
display: flex;
flex-direction: column;
gap: 30px;
}
.description p {
font-size: 0.95rem;
line-height: 1.8;
margin: 0 0 15px 0;
color: var(--color-text-primary);
}
.specs-and-amenities {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 50px;
}
.spec-list .item {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
padding: 12px 0;
border-bottom: 1px solid var(--color-border);
}
.spec-list .item:first-child { padding-top: 0; }
.spec-list .item .key { font-weight: 600; color: var(--color-text-secondary); }
.spec-list .item .value { font-weight: 400; color: var(--color-white); }
.amenities-list { list-style: none; padding: 0; margin: 0; columns: 2; gap: 15px; }
.amenities-list li { font-size: 0.9rem; margin-bottom: 15px; display: flex; align-items: center; }
.amenities-list i { color: var(--color-accent-gold); margin-right: 12px; font-size: 1.1rem; }
/* --- PAGE 3: LOCATION (NEW LAYOUT) --- */
.location-body { padding: 0; } /* Override default padding */
.location-map-container {
height: 45%;
background-image: url('https://images.unsplash.com/photo-1549880181-56a44cf4a9a5?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
background-size: cover;
background-position: center 75%;
position: relative;
}
.location-map-container::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 70%;
background: linear-gradient(180deg, rgba(18, 18, 18, 0) 0%, var(--color-dark-charcoal) 90%);
}
.location-content { padding: var(--padding-page); }
.poi-grid {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: 30px; margin-top: 40px;
}
.poi-item { text-align: center; }
.poi-item .icon {
font-size: 2.5rem; color: var(--color-accent-gold); margin-bottom: 15px;
}
.poi-item .title {
font-weight: 600; font-size: 1rem; color: var(--color-white); margin-bottom: 5px;
}
.poi-item .details { font-size: 0.9rem; color: var(--color-text-secondary); }
/* --- PAGE 4: LAYOUT & LIFESTYLE (REVISED) --- */
.page-split-layout {
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 50px;
height: 100%;
}
.gallery-section, .additional-info-section {
display: flex;
flex-direction: column;
}
.additional-info-section .spec-list {
margin-top: 25px;
}
.photo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 20px;
margin-top: 25px;
flex-grow: 1;
}
.photo-item {
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--color-border);
background-size: cover;
background-position: center;
}
.photo-item-1 {
grid-column: 1 / -1; /* Span full width */
background-image: url('https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
}
.photo-item-2 {
background-image: url('https://images.unsplash.com/photo-1600585152225-3579fe9d7ae2?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
}
.photo-item-3 {
background-image: url('https://images.unsplash.com/photo-1600585153325-1a75f8a4f631?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
}
</style>
</head>
<body>
<div class="brochure-page cover-page">
<div class="cover-overlay"></div>
<header class="cover-header"><div class="logo">Elysian</div><div class="property-status">FOR SALE</div></header>
<main class="cover-content">
<h1 class="cover-title">The Grand Oak Villa</h1>
<p class="cover-address"><i class="fa-solid fa-location-dot"></i> 123 Luxury Lane, Prestige City, PC 45678</p>
</main>
<footer class="cover-footer">
<div class="feature-item"><div class="value">5</div><div class="label">Bedrooms</div></div>
<div class="feature-item"><div class="value">6</div><div class="label">Bathrooms</div></div>
<div class="feature-item"><div class="value">6,200</div><div class="label">Sq. Ft.</div></div>
<div class="feature-item"><div class="value">$4,500,000</div><div class="label">Price</div></div>
</footer>
</div>
<div class="brochure-page">
<div class="content-body">
<header class="page-header"><h1 class="title">Property <span>Overview</span></h1><span class="property-name">The Grand Oak Villa</span></header>
<main class="main-content details-grid">
<div>
<h2 class="section-title">Description</h2>
<div class="description">
<p>Nestled in the heart of Prestige City, The Grand Oak Villa is a masterpiece of modern architecture and timeless elegance. This expansive 6,200 sq. ft. residence offers unparalleled luxury and privacy.</p>
<p>With soaring ceilings, bespoke finishes, and panoramic views from every room, this home is designed for those who appreciate the finer things in life. The open-plan living space is perfect for entertaining, featuring a gourmet chef's kitchen, a formal dining area, and a grand living room with a statement fireplace.</p>
</div>
</div>
<div class="specs-and-amenities">
<div>
<h2 class="section-title">Specifications</h2>
<div class="spec-list">
<div class="item"><span class="key">Reference ID:</span> <span class="value">[Reference ID]</span></div>
<div class="item"><span class="key">Status:</span> <span class="value">[Status]</span></div>
<div class="item"><span class="key">Type:</span> <span class="value">[Property Type]</span></div>
<div class="item"><span class="key">Year Built:</span> <span class="value">[Year Built]</span></div>
<div class="item"><span class="key">Floor:</span> <span class="value">[Floor]</span></div>
<div class="item"><span class="key">Parking:</span> <span class="value">[Parking]</span></div>
<div class="item"><span class="key">Furnishing:</span> <span class="value">[Furnishing]</span></div>
<div class="item"><span class="key">Maintenance Fee:</span> <span class="value">[Maintenance Fee]</span></div>
<div class="item"><span class="key">Service Charge:</span> <span class="value">[Service Charge]</span></div>
</div>
</div>
<div>
<h2 class="section-title">Amenities & Features</h2>
<ul class="amenities-list">
<li><i class="fa-solid fa-check"></i> Infinity Pool</li><li><i class="fa-solid fa-check"></i> Private Home Theater</li><li><i class="fa-solid fa-check"></i> Gourmet Chef's Kitchen</li><li><i class="fa-solid fa-check"></i> Wine Cellar</li><li><i class="fa-solid fa-check"></i> Smart Home Automation</li><li><i class="fa-solid fa-check"></i> Spa & Sauna Room</li><li><i class="fa-solid fa-check"></i> Landscaped Gardens</li><li><i class="fa-solid fa-check"></i> Outdoor Fire Pit</li>
</ul>
</div>
</div>
</main>
</div>
<footer class="page-footer">
<div><strong>Agent:</strong> [Agent Name] | [Agent Phone] | [Agent Email]</div>
<div><strong>Owner:</strong> [Owner Name] | [Owner Phone] | [Owner Email]</div>
</footer>
</div>
<div class="brochure-page">
<div class="content-body location-body">
<div class="location-map-container"></div>
<div class="location-content">
<header class="page-header" style="margin-bottom: 0;">
<h1 class="title">An Unrivaled <span>Setting</span></h1>
<span class="property-name">123 Luxury Lane, Prestige City</span>
</header>
<div class="poi-grid">
<div class="poi-item">
<div class="icon"><i class="fa-solid fa-school"></i></div>
<div class="title">Schools</div>
<div class="details">[Schools]</div>
</div>
<div class="poi-item">
<div class="icon"><i class="fa-solid fa-shopping-cart"></i></div>
<div class="title">Shopping</div>
<div class="details">[Shopping Centers]</div>
</div>
<div class="poi-item">
<div class="icon"><i class="fa-solid fa-plane"></i></div>
<div class="title">Airport</div>
<div class="details">[Airport Distance]</div>
</div>
<div class="poi-item">
<div class="icon"><i class="fa-solid fa-landmark"></i></div>
<div class="title">Landmarks</div>
<div class="details">[Nearby Landmarks]</div>
</div>
<div class="poi-item">
<div class="icon"><i class="fa-solid fa-bus"></i></div>
<div class="title">Transportation</div>
<div class="details">[Transportation]</div>
</div>
<div class="poi-item">
<div class="icon"><i class="fa-solid fa-hospital"></i></div>
<div class="title">Hospitals</div>
<div class="details">[Hospitals]</div>
</div>
<div class="poi-item">
<div class="icon"><i class="fa-solid fa-umbrella-beach"></i></div>
<div class="title">Beach</div>
<div class="details">[Beach Distance]</div>
</div>
<div class="poi-item">
<div class="icon"><i class="fa-solid fa-subway"></i></div>
<div class="title">Metro</div>
<div class="details">[Metro Distance]</div>
</div>
</div>
</div>
</div>
<footer class="page-footer">
<div><strong>Agent:</strong> [Agent Name] | [Agent Phone] | [Agent Email]</div>
<div><strong>Owner:</strong> [Owner Name] | [Owner Phone] | [Owner Email]</div>
</footer>
</div>
<div class="brochure-page">
<div class="content-body">
<header class="page-header">
<h1 class="title">Layout & <span>Lifestyle</span></h1>
<span class="property-name">The Grand Oak Villa</span>
</header>
<main class="main-content page-split-layout">
<section class="gallery-section">
<h2 class="section-title">A Glimpse Inside</h2>
<div class="photo-grid">
<div class="photo-item photo-item-1"></div>
<div class="photo-item photo-item-2"></div>
<div class="photo-item photo-item-3"></div>
</div>
</section>
<section class="additional-info-section">
<h2 class="section-title">Additional Information</h2>
<div class="spec-list">
<div class="item"><span class="key">Pet Friendly:</span> <span class="value">[Pet Friendly Status]</span></div>
<div class="item"><span class="key">Smoking:</span> <span class="value">[Smoking Allowed]</span></div>
<div class="item"><span class="key">Available From:</span> <span class="value">[Available From Date]</span></div>
<div class="item"><span class="key">Minimum Contract:</span> <span class="value">[Minimum Contract Duration]</span></div>
<div class="item"><span class="key">Security Deposit:</span> <span class="value">[Security Deposit]</span></div>
<div class="item"><span class="key">Utilities Included:</span> <span class="value">[Utilities Included]</span></div>
<div class="item"><span class="key">Internet Included:</span> <span class="value">[Internet Included]</span></div>
<div class="item"><span class="key">Cable Included:</span> <span class="value">[Cable Included]</span></div>
</div>
</section>
</main>
</div>
<footer class="page-footer">
<div><strong>Agent:</strong> [Agent Name] | [Agent Phone] | [Agent Email]</div>
<div><strong>Owner:</strong> [Owner Name] | [Owner Phone] | [Owner Email]</div>
</footer>
</div>
</body>
</html>

View File

@ -1,426 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Editorial Real Estate Brochure - Updated</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&family=Cormorant+Garamond:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
/* --- DESIGN SYSTEM & VARIABLES --- */
:root {
/* Color Palette */
--color-bg: #FFFFFF;
--color-off-white: #F8F7F5;
--color-text-primary: #333333;
--color-text-secondary: #777777;
--color-accent-beige: #D4C7B8;
--color-border: #EAEAEA;
/* Typography */
--font-serif: 'Cormorant Garamond', serif;
--font-sans: 'Lato', sans-serif;
}
/* --- GLOBAL & BODY STYLES --- */
body {
font-family: var(--font-sans);
background-color: #d8d8d8;
display: flex;
flex-direction: column;
align-items: center;
padding: 50px;
margin: 0;
gap: 50px;
}
.brochure-page {
width: 210mm;
height: 297mm;
background-color: var(--color-bg);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* --- PAGE 1: FRONT COVER --- */
.p1-container {
display: flex;
height: 100%;
}
.p1-image-side {
flex: 1.2;
background-image: url('https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
background-size: cover;
background-position: center;
}
.p1-content-side {
flex: 1;
padding: 70px 60px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.p1-header .collection {
font-size: 0.9rem;
letter-spacing: 3px;
color: var(--color-text-secondary);
text-transform: uppercase;
}
.p1-main-title {
font-family: var(--font-serif);
font-size: 5rem;
font-weight: 600;
line-height: 1.1;
color: var(--color-text-primary);
margin: 20px 0;
}
.p1-address {
font-size: 1rem;
color: var(--color-text-secondary);
border-left: 3px solid var(--color-accent-beige);
padding-left: 20px;
}
.p1-ref-id {
font-size: 0.9rem;
color: var(--color-text-secondary);
margin-top: 15px;
padding-left: 23px;
}
.p1-footer {
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.p1-footer strong {
color: var(--color-text-primary);
}
.p1-footer .area {
font-size: 1rem;
color: var(--color-text-primary);
font-weight: 700;
margin-bottom: 10px;
}
/* --- SHARED STYLES for Content Pages --- */
.page-layout {
padding: 70px;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
}
.page-number {
position: absolute;
top: 70px; right: 70px;
font-family: var(--font-serif);
font-size: 1.2rem;
color: var(--color-text-secondary);
}
.page-title-main {
font-family: var(--font-serif);
font-size: 3.5rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 15px 0;
line-height: 1;
}
.page-title-sub {
font-size: 1rem;
color: var(--color-text-secondary);
margin-bottom: 50px;
}
.content-divider {
border: 0;
height: 1px;
background-color: var(--color-border);
margin: 30px 0;
}
.section-title {
font-family: var(--font-serif);
font-size: 1.5rem;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 20px;
margin-top: 0;
}
/* --- PAGE 2: INTRODUCTION & NARRATIVE --- */
.p2-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
flex-grow: 1;
}
.p2-image {
background-image: url('https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800');
background-size: cover;
background-position: center;
}
.p2-text p {
font-size: 1rem;
line-height: 1.8;
color: var(--color-text-secondary);
}
.p2-text p:first-of-type::first-letter {
font-family: var(--font-serif);
font-size: 4rem;
float: left;
line-height: 1;
margin-right: 15px;
color: var(--color-accent-beige);
}
.pull-quote {
border-left: 3px solid var(--color-accent-beige);
padding-left: 25px;
margin: 30px 0;
font-family: var(--font-serif);
font-size: 1.5rem;
font-style: italic;
color: var(--color-text-primary);
}
/* --- PAGE 3: DETAILS & AMENITIES --- */
.p3-main-content { flex-grow: 1; }
.spec-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30px;
padding: 30px;
background-color: var(--color-off-white);
}
.spec-item { text-align: center; }
.spec-item .value {
font-size: 2rem;
font-weight: 700;
color: var(--color-text-primary);
}
.spec-item .label {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--color-text-secondary);
}
.details-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px 40px;
}
.details-item {
display: flex;
justify-content: space-between;
border-bottom: 1px solid var(--color-border);
padding-bottom: 10px;
font-size: 0.9rem;
}
.details-item .label { color: var(--color-text-secondary); }
.details-item .value { color: var(--color-text-primary); font-weight: 700; }
.amenities-list { list-style: none; padding: 0; column-count: 2; column-gap: 40px;}
.amenities-list li {
margin-bottom: 12px;
color: var(--color-text-secondary);
display: flex;
align-items: center;
font-size: 0.9rem;
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
break-inside: avoid;
}
.amenities-list li i { color: var(--color-accent-beige); margin-right: 12px; }
/* --- PAGE 4: REVISED LAYOUT --- */
.p4-section-title {
font-size: 1rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--color-text-primary);
margin: 0 0 20px 0;
}
.p4-floorplan-container {
height: 320px;
background-color: var(--color-off-white);
border: 1px solid var(--color-border);
background-image: url('https://cdn.shopify.com/s/files/1/0024/0495/3953/files/Architect_s_floor_plan_for_a_house_in_black_and_white_large.jpg');
background-size: contain;
background-position: center;
background-repeat: no-repeat;
margin-bottom: 40px;
}
.p4-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
margin-bottom: 40px;
}
.info-list .info-item, .location-list .item {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid var(--color-border);
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.info-list .info-item strong, .location-list .item strong {
color: var(--color-text-primary);
margin-right: 15px;
}
.p4-contact-row {
display: flex;
gap: 40px;
justify-content: center;
margin-top: auto;
}
.contact-card {
background-color: var(--color-off-white);
padding: 20px;
text-align: center;
flex: 1;
max-width: 300px;
}
.contact-card .title {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--color-text-secondary);
margin-bottom: 8px;
}
.contact-card .name { font-family: var(--font-serif); font-size: 1.5rem; font-weight: 600; }
.contact-card .phone, .contact-card .email { font-size: 0.9rem; margin: 4px 0; color: var(--color-text-secondary); }
</style>
</head>
<body>
<div class="brochure-page">
<div class="p1-container">
<div class="p1-image-side"></div>
<div class="p1-content-side">
<header class="p1-header">
<div class="collection">Elysian Estates Collection</div>
<h1 class="p1-main-title">The Serenity House</h1>
<p class="p1-address">123 Luxury Lane, Prestige City, PC 45678</p>
<p class="p1-ref-id">Reference ID: ES-8821</p>
</header>
<footer class="p1-footer">
<div class="area">6,200 Sq. Ft. • 5 Bedrooms • 6 Bathrooms</div>
An architectural marvel of curated living space.
<br>
<strong>Offered at $4,500,000</strong>
</footer>
</div>
</div>
</div>
<div class="brochure-page">
<div class="page-layout">
<span class="page-number">02</span>
<h1 class="page-title-main">A Sanctuary of Modern Design</h1>
<p class="page-title-sub">Where light, space, and nature converge to create an unparalleled living experience.</p>
<div class="p2-grid">
<div class="p2-text">
<p>Designed by the world-renowned architect, Helena Vance, The Serenity House is more than a home; it is a living sculpture. Every line, material, and detail has been thoughtfully considered to evoke a sense of peace and connection with the surrounding landscape. Soaring ceilings and floor-to-ceiling glass walls dissolve the boundaries between inside and out, flooding the space with natural light.</p>
<p class="pull-quote">A timeless residence built not just for living, but for thriving.</p>
<p>The interior palette is a harmonious blend of natural oak, Italian travertine, and warm bronze accents, creating an atmosphere of understated luxury. This property represents a unique opportunity to own a piece of architectural history.</p>
</div>
<div class="p2-image"></div>
</div>
</div>
</div>
<div class="brochure-page">
<div class="page-layout">
<span class="page-number">03</span>
<h1 class="page-title-main">Property Specifications</h1>
<p class="page-title-sub">A comprehensive overview of the propertys features, details, and amenities.</p>
<div class="p3-main-content">
<div class="spec-grid">
<div class="spec-item"><div class="value">5</div><div class="label">Bedrooms</div></div>
<div class="spec-item"><div class="value">6</div><div class="label">Bathrooms</div></div>
<div class="spec-item"><div class="value">6,200</div><div class="label">Square Feet</div></div>
<div class="spec-item"><div class="value">0.75</div><div class="label">Acres</div></div>
</div>
<hr class="content-divider">
<h3 class="section-title">Property Details</h3>
<div class="details-grid">
<div class="details-item"><span class="label">Status</span><span class="value">For Sale</span></div>
<div class="details-item"><span class="label">Year Built</span><span class="value">2023</span></div>
<div class="details-item"><span class="label">Type</span><span class="value">Single-Family Home</span></div>
<div class="details-item"><span class="label">Furnishing</span><span class="value">Partially Furnished</span></div>
<div class="details-item"><span class="label">Floor</span><span class="value">2 Levels</span></div>
<div class="details-item"><span class="label">Maintenance Fee</span><span class="value">$1,200 / month</span></div>
<div class="details-item"><span class="label">Parking</span><span class="value">3-Car Garage</span></div>
<div class="details-item"><span class="label">Service Charge</span><span class="value">Included</span></div>
</div>
<hr class="content-divider">
<h3 class="section-title">Amenities & Features</h3>
<ul class="amenities-list">
<li><i class="fa-solid fa-check"></i> Primary Suite with Spa-Bath</li>
<li><i class="fa-solid fa-check"></i> Radiant Heated Flooring</li>
<li><i class="fa-solid fa-check"></i> Custom Walk-in Closets</li>
<li><i class="fa-solid fa-check"></i> Smart Home Automation</li>
<li><i class="fa-solid fa-check"></i> Infinity Edge Saline Pool</li>
<li><i class="fa-solid fa-check"></i> Private Cinema Room</li>
<li><i class="fa-solid fa-check"></i> Temperature-Controlled Wine Cellar</li>
<li><i class="fa-solid fa-check"></i> Landscaped Gardens & Terrace</li>
<li><i class="fa-solid fa-check"></i> Gourmet Chef's Kitchen</li>
<li><i class="fa-solid fa-check"></i> Floor-to-Ceiling Glass Walls</li>
</ul>
</div>
</div>
</div>
<div class="brochure-page">
<div class="page-layout">
<span class="page-number">04</span>
<h1 class="page-title-main" style="margin-bottom: 30px;">Floor Plan & Details</h1>
<div class="p4-info-grid">
<div class="location-list">
<h2 class="p4-section-title">Location & Nearby</h2>
<div class="item"><strong>Schools</strong> <span>5 min drive</span></div>
<div class="item"><strong>Shopping</strong> <span>10 min drive</span></div>
<div class="item"><strong>Hospitals</strong> <span>12 min drive</span></div>
<div class="item"><strong>Country Club</strong> <span>8 min drive</span></div>
<div class="item"><strong>Airport</strong> <span>20 min drive</span></div>
</div>
<div class="info-list">
<h2 class="p4-section-title">Additional Information</h2>
<div class="info-item"><strong>Pet-Friendly</strong> <span>By Approval</span></div>
<div class="info-item"><strong>Smoking</strong> <span>Not Permitted</span></div>
<div class="info-item"><strong>Availability</strong> <span>Immediate</span></div>
<div class="info-item"><strong>Utilities</strong> <span>Not Included</span></div>
</div>
</div>
<hr class="content-divider">
<h2 class="p4-section-title">Floor Plan & Location</h2>
<div class="p4-floorplan-container"></div>
<div class="p4-contact-row">
<div class="contact-card">
<div class="title">Owner Information</div>
<div class="name">John & Jane Doe</div>
<p class="phone">(555) 111-2222</p>
<p class="email">owner.serenity@email.com</p>
</div>
<div class="contact-card">
<div class="title">Agent Information</div>
<div class="name">Olivia Sterling</div>
<p class="phone">(555) 987-6543</p>
<p class="email">olivia@elysianestates.com</p>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,604 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern Urban Residences Brochure - Updated</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
/* --- DESIGN SYSTEM & VARIABLES --- */
:root {
/* Color Palette */
--color-dark: #111111;
--color-light: #FFFFFF;
--color-accent-green: #0A6847;
--color-grey-bg: #F0F0F0;
--color-text-dark: #222222;
--color-text-light: #EFEFEF;
--color-text-muted: #888888;
--color-border: #DDDDDD;
/* Typography */
--font-main: 'Inter', sans-serif;
}
/* --- GLOBAL & BODY STYLES --- */
body {
font-family: var(--font-main);
background-color: #d8d8d8;
display: flex;
flex-direction: column;
align-items: center;
padding: 50px;
margin: 0;
gap: 50px;
}
.brochure-page {
width: 210mm;
height: 297mm;
background-color: var(--color-light);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* --- PAGE 1: COVER PAGE --- */
.cover-page {
background-image: url('https://plus.unsplash.com/premium_photo-1677474827617-6a7269f97574?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
background-size: cover;
background-position: center;
color: var(--color-light);
justify-content: center;
align-items: center;
text-align: center;
position: relative;
}
.cover-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.3));
}
.cover-content {
position: relative; z-index: 2; padding: 50px;
}
.cover-content .subtitle {
font-size: 1rem;
font-weight: 500;
letter-spacing: 4px;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.8);
}
.cover-content .main-title {
font-size: 6rem;
font-weight: 800;
line-height: 1.1;
margin: 10px 0 20px 0;
text-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
.cover-content .address {
font-size: 1.1rem;
font-weight: 400;
border-top: 1px solid var(--color-accent-green);
display: inline-block;
padding-top: 20px;
}
.cover-footer {
position: absolute;
bottom: 40px;
left: 40px; right: 40px;
z-index: 2;
display: flex;
justify-content: space-between;
font-size: 0.9rem;
font-weight: 500;
}
/* --- SHARED STYLES --- */
.page-container {
padding: 70px;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: baseline;
border-bottom: 1px solid var(--color-border);
margin-bottom: 20px;
}
.page-title {
font-size: 2.5rem;
font-weight: 800;
color: var(--color-dark);
}
.page-title span {
color: var(--color-accent-green);
}
.page-subtitle {
font-size: 1rem;
font-weight: 500;
color: var(--color-text-muted);
}
.page-footer-bar {
margin-top: auto;
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20px;
border-top: 1px solid var(--color-border);
font-size: 0.9rem;
font-weight: 500;
color: var(--color-text-muted);
}
.page-footer-bar .property-name {
color: var(--color-dark);
}
.section-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-dark);
margin-bottom: 25px;
margin-top: 0;
}
.detail-item {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
padding: 12px 0;
border-bottom: 1px solid var(--color-border);
}
.detail-item .label { color: var(--color-text-muted); }
.detail-item .value { color: var(--color-text-dark); font-weight: 600; text-align: right; }
/* --- PAGE 2: THE VISION --- */
.vision-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
flex-grow: 1;
margin-bottom: 20px;
}
.vision-image {
background-image: url('https://images.unsplash.com/photo-1626704359446-0de90350b4e7?q=80&w=736&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
background-size: cover;
background-position: center;
}
.vision-text h3 {
font-size: 1.8rem;
font-weight: 700;
color: var(--color-dark);
margin-bottom: 20px;
}
.vision-text p {
font-size: 1rem;
line-height: 1.8;
color: var(--color-text-muted);
margin-bottom: 20px;
}
/* --- PAGE 3: RESIDENCES GALLERY --- */
.gallery-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 250px 250px 250px;
gap: 20px;
flex-grow: 1;
padding-top: 5px;
}
.gallery-item {
background-size: cover;
background-position: center;
position: relative;
display: flex;
align-items: flex-end;
color: var(--color-light);
padding: 15px;
}
.gallery-item::after {
content: ''; position: absolute; top:0; left: 0; width: 100%; height: 100%;
background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);
}
.gallery-item span { font-weight: 600; z-index: 2; }
.g-item-1 { grid-column: 1 / 3; grid-row: 1 / 2; background-image: url('https://images.unsplash.com/photo-1616046229478-9901c5536a45?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
.g-item-2 { grid-column: 3 / 4; grid-row: 1 / 3; background-image: url('https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
.g-item-3 { grid-column: 1 / 2; grid-row: 2 / 4; background-image: url('https://images.unsplash.com/photo-1600121848594-d8644e57abab?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
.g-item-4 { grid-column: 2 / 3; grid-row: 2 / 3; background-image: url('https://images.unsplash.com/photo-1595526114035-0d45ed16433d?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
.g-item-5 { grid-column: 2 / 4; grid-row: 3 / 4; background-image: url('https://images.unsplash.com/photo-1512918728675-ed5a71a580a9?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
/* --- PAGE 4: AMENITIES & FEATURES (REVISED V2) --- */
.amenities-intro {
font-size: 1rem;
line-height: 1.8;
color: var(--color-text-muted);
margin: 10px 0 20px 0;
}
.page4-grid {
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 50px;
flex-grow: 1;
margin-bottom: 30px;
}
.page4-image {
background-image: url('https://plus.unsplash.com/premium_photo-1675745330187-a6f001a21abe?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
background-size: cover;
background-position: center;
min-height: 100%;
border: 6px solid var(--color-accent-green);
}
.page4-content {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.amenities-list-p4 {
list-style: none;
padding: 0;
margin: 0 0 30px 0;
column-count: 2;
column-gap: 30px;
}
.amenities-list-p4 li {
display: flex;
align-items: flex-start;
margin-bottom: 18px;
font-weight: 500;
font-size: 0.95rem;
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
break-inside: avoid;
}
.amenities-list-p4 i {
font-size: 1rem;
color: var(--color-accent-green);
margin-right: 15px;
width: 20px;
margin-top: 3px;
}
/* --- PAGE 5: FLOOR PLANS & SPECS (REVISED V2) --- */
.page5-main {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 25px;
}
.floorplan-showcase {
display: grid;
grid-template-columns: 220px 1fr;
gap: 25px;
background-color: var(--color-grey-bg);
padding: 20px;
border-left: 5px solid var(--color-accent-green);
}
.floorplan-image-p5 {
background-size: cover;
background-position: center;
min-height: 200px;
}
.floorplan-image-p5.residence {
background-image: url('https://images.unsplash.com/photo-1740446568651-1d31966b228a?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
}
.floorplan-image-p5.penthouse {
background-image: url('https://images.unsplash.com/photo-1721522281545-fad32dd5107a?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
}
.floorplan-info-p5 h4 {
font-size: 1.3rem;
font-weight: 700;
margin: 0 0 15px 0;
}
.floorplan-stats-p5 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid var(--color-border);
}
.floorplan-stats-p5 .stat .value {
font-weight: 700;
font-size: 1.3rem;
}
.floorplan-stats-p5 .stat .label {
font-size: 0.8rem;
color: var(--color-text-muted);
text-transform: uppercase;
}
.floorplan-info-p5 .description {
font-size: 0.9rem;
color: var(--color-text-muted);
line-height: 1.7;
}
.additional-specs-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-top: 15px;
}
.spec-item {
background-color: var(--color-grey-bg);
padding: 15px;
}
.spec-item .label {
font-size: 0.8rem;
color: var(--color-text-muted);
margin-bottom: 5px;
display: block;
}
.spec-item .value {
font-weight: 600;
font-size: 1rem;
color: var(--color-dark);
}
/* --- PAGE 6: LOCATION & CONTACT (REVISED V3) --- */
.page6-main {
flex-grow: 1;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
align-content: start;
padding-top: 20px;
}
.page6-main p {
font-size: 0.95rem;
line-height: 1.8;
color: var(--color-text-muted);
margin-bottom: 30px;
}
.contact-person-p6 {
margin-bottom: 30px;
}
.contact-person-p6 .name {
font-weight: 700;
font-size: 1.3rem;
color: var(--color-dark);
}
.contact-person-p6 .title {
color: var(--color-text-muted);
font-size: 0.9rem;
margin-bottom: 10px;
}
.contact-person-p6 .details {
font-weight: 500;
font-size: 1rem;
line-height: 1.7;
}
.highlights-list-p6 {
list-style: none;
padding: 0;
margin: 0;
}
.highlights-list-p6 li {
display: flex;
align-items: flex-start;
margin-bottom: 18px;
font-size: 0.95rem;
}
.highlights-list-p6 i {
color: var(--color-accent-green);
margin-right: 15px;
width: 20px;
margin-top: 4px;
}
</style>
</head>
<body>
<div class="brochure-page cover-page">
<div class="cover-overlay"></div>
<div class="cover-content">
<div class="subtitle">An Urban Oasis</div>
<h1 class="main-title">THE VERTICE</h1>
<div class="address">18 Skyline Avenue, Metropolis Centre, MC 90210</div>
</div>
<div class="cover-footer">
<span>Residences Starting from $1,200,000</span>
<span>Ref ID: VP-2025-001</span>
</div>
</div>
<div class="brochure-page">
<div class="page-container">
<header class="page-header">
<h1 class="page-title">Elevated <span>Living</span></h1>
<span class="page-subtitle">Discover Your Sanctuary in the Sky</span>
</header>
<main class="vision-grid">
<div class="vision-text">
<h3>Where Design Meets Desire.</h3>
<p>The Vertice is not just a building; it's a bold statement on modern urban living. Conceived for the discerning individual, it offers a unique blend of architectural prowess, bespoke interiors, and an unparalleled lifestyle experience right in the heart of the city.</p>
<p>Every residence is a testament to quality, featuring panoramic city views from floor-to-ceiling windows, intelligent home systems, and finishes selected from the finest materials around the globe. This is more than a home; it's a new perspective.</p>
</div>
<div class="vision-image"></div>
</main>
<footer class="page-footer-bar">
<span class="property-name">THE VERTICE</span>
<span>Page 02 / 06</span>
</footer>
</div>
</div>
<div class="brochure-page">
<div class="page-container">
<header class="page-header">
<h1 class="page-title">Exquisite <span>Interiors</span></h1>
<span class="page-subtitle">A Canvas for Your Life</span>
</header>
<main class="gallery-grid">
<div class="gallery-item g-item-1"><span>Open-Concept Living Space</span></div>
<div class="gallery-item g-item-2"><span>Master Bedroom Suite</span></div>
<div class="gallery-item g-item-3"><span>Gourmet Chef's Kitchen</span></div>
<div class="gallery-item g-item-4"><span>Spa-Inspired Bathroom</span></div>
<div class="gallery-item g-item-5"><span>Private Balcony Views</span></div>
</main>
<footer class="page-footer-bar">
<span class="property-name">THE VERTICE</span>
<span>Page 03 / 06</span>
</footer>
</div>
</div>
<div class="brochure-page">
<div class="page-container">
<header class="page-header">
<h1 class="page-title">Amenities & <span>Features</span></h1>
<span class="page-subtitle">Beyond the Expected</span>
</header>
<p class="amenities-intro">An unrivaled collection of amenities offers residents a resort-style living experience. From the serene rooftop pool to the state-of-the-art wellness center, every detail is crafted for comfort, convenience, and luxury.</p>
<main class="page4-grid">
<div class="page4-image"></div>
<div class="page4-content">
<div>
<h3 class="section-title">Lifestyle Amenities</h3>
<ul class="amenities-list-p4">
<li><i class="fa-solid fa-water-ladder"></i> Rooftop Infinity Pool</li>
<li><i class="fa-solid fa-dumbbell"></i> Fitness Center</li>
<li><i class="fa-solid fa-martini-glass-empty"></i> Residents' Sky Lounge</li>
<li><i class="fa-solid fa-film"></i> Private Cinema Room</li>
<li><i class="fa-solid fa-spa"></i> Wellness Spa & Sauna</li>
<li><i class="fa-solid fa-briefcase"></i> Business Center</li>
<li><i class="fa-solid fa-shield-halved"></i> 24/7 Concierge</li>
<li><i class="fa-solid fa-car"></i> Secure Parking</li>
</ul>
</div>
<div>
<h3 class="section-title">Key Specifications</h3>
<div class="detail-item"><span class="label">Status</span> <span class="value">New Development</span></div>
<div class="detail-item"><span class="label">Property Type</span> <span class="value">Condominium</span></div>
<div class="detail-item"><span class="label">Year Built</span> <span class="value">2025</span></div>
<div class="detail-item"><span class="label">Technology</span> <span class="value">Integrated Smart Home</span></div>
<div class="detail-item"><span class="label">Design</span> <span class="value">Sustainable & Eco-Friendly</span></div>
</div>
</div>
</main>
<footer class="page-footer-bar">
<span class="property-name">THE VERTICE</span>
<span>Page 04 / 06</span>
</footer>
</div>
</div>
<div class="brochure-page">
<div class="page-container">
<header class="page-header">
<h1 class="page-title">Floor Plans & <span>Details</span></h1>
<span class="page-subtitle">Designed for Modern Life</span>
</header>
<main class="page5-main">
<div class="floorplan-showcase">
<div class="floorplan-image-p5 residence"></div>
<div class="floorplan-info-p5">
<h4>Two-Bedroom Residence</h4>
<div class="floorplan-stats-p5">
<div class="stat">
<div class="value">1,450</div>
<div class="label">SQ. FT.</div>
</div>
<div class="stat">
<div class="value">2</div>
<div class="label">BEDROOMS</div>
</div>
<div class="stat">
<div class="value">2</div>
<div class="label">BATHROOMS</div>
</div>
<div class="stat">
<div class="value">1</div>
<div class="label">BALCONY</div>
</div>
</div>
<p class="description">A thoughtfully designed space perfect for urban professionals or small families, combining comfort with panoramic city views.</p>
</div>
</div>
<div class="floorplan-showcase">
<div class="floorplan-image-p5 penthouse"></div>
<div class="floorplan-info-p5">
<h4>Three-Bedroom Penthouse</h4>
<div class="floorplan-stats-p5">
<div class="stat">
<div class="value">3,200</div>
<div class="label">SQ. FT.</div>
</div>
<div class="stat">
<div class="value">3</div>
<div class="label">BEDROOMS</div>
</div>
<div class="stat">
<div class="value">3.5</div>
<div class="label">BATHROOMS</div>
</div>
<div class="stat">
<div class="value">1</div>
<div class="label">TERRACE</div>
</div>
</div>
<p class="description">The pinnacle of luxury living, this penthouse offers expansive spaces, premium finishes, and exclusive access to a private rooftop terrace.</p>
</div>
</div>
<div>
<h3 class="section-title">Additional Information</h3>
<div class="additional-specs-grid">
<div class="spec-item"><div class="label">Pets</div><div class="value">Allowed (w/ restrictions)</div></div>
<div class="spec-item"><div class="label">Smoking</div><div class="value">In designated areas</div></div>
<div class="spec-item"><div class="label">Availability</div><div class="value">Q4 2025</div></div>
<div class="spec-item"><div class="label">Parking</div><div class="value">2 Spaces per Unit</div></div>
<div class="spec-item"><div class="label">Security Deposit</div><div class="value">2 Months</div></div>
<div class="spec-item"><div class="label">Utilities</div><div class="value">Sub-metered</div></div>
</div>
</div>
</main>
<footer class="page-footer-bar">
<span class="property-name">THE VERTICE</span>
<span>Page 05 / 06</span>
</footer>
</div>
</div>
<div class="brochure-page">
<div class="page-container">
<header class="page-header">
<h1 class="page-title">Location & <span>Inquiry</span></h1>
<span class="page-subtitle">Your Future Awaits</span>
</header>
<main class="page6-main">
<div>
<h3 class="section-title">Schedule a Private Viewing</h3>
<p>Experience The Vertice firsthand. Contact our sales executive to arrange an exclusive tour of the property and available residences.</p>
<div class="contact-person-p6">
<div class="name">Alexander Valentine</div>
<div class="title">Sales Executive, Elysian Properties</div>
<div class="details">
(555) 123-9876<br>
alex.v@elysian.com
</div>
</div>
</div>
<div>
<h3 class="section-title">Neighborhood Highlights</h3>
<ul class="highlights-list-p6">
<li><i class="fa-solid fa-tree-city"></i> <strong>Landmarks:</strong> Central Park (5 min)</li>
<li><i class="fa-solid fa-train-subway"></i> <strong>Transportation:</strong> Metro Line A (2 min walk)</li>
<li><i class="fa-solid fa-school"></i> <strong>Schools:</strong> Metropolis Intl. (10 min)</li>
<li><i class="fa-solid fa-cart-shopping"></i> <strong>Shopping:</strong> The Galleria Mall (8 min)</li>
<li><i class="fa-solid fa-plane-departure"></i> <strong>Airport:</strong> 25 min drive</li>
</ul>
</div>
</main>
<footer class="page-footer-bar">
<span class="property-name">THE VERTICE</span>
<span>Page 06 / 06</span>
</footer>
</div>
</div>
</body>
</html>