mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-18 04:08:48 -05:00
fix(pdf-layout): decorative image persistence and PDF preview (Issue #432)
Decorative images now survive save/load and no longer cause a black PDF preview: - Sync imageUrl onto groups before generateCode() so template_json has correct source (invoice and quote layout editors). - Inject name/imageUrl into design_json with position-based matching so reordering does not swap or drop URLs. - Restore name and imageUrl from saved JSON onto canvas on load (synchronous) so Konva custom attrs are not required. - Omit decorative image elements with empty source from template_json; placeholders stay visible in the editor but are not sent to ReportLab. - ReportLab: explicitly skip decorative images with empty source; validate base64 data URI payload and decode in try/except to avoid bad PDF output. Documentation: PDF_LAYOUT_CUSTOMIZATION.md and PDF_EDITOR_ENHANCED_FEATURES.md updated with decorative image description, state persistence details, and troubleshooting. CHANGELOG.md updated under [Unreleased] Fixed.
This commit is contained in:
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **PDF layout: decorative image persistence and PDF preview (Issue #432)** — Decorative images now survive save/load: image URLs are synced onto groups before generating the template, injected into the saved design JSON using position-based matching, and restored from the saved JSON onto the canvas on load. Empty decorative image elements are no longer added to the ReportLab template, and the PDF generator skips empty or invalid image sources and validates base64 data URIs, preventing a mostly-black or broken PDF preview.
|
||||
|
||||
### Added
|
||||
- **ZugFerd / Factur-X support for invoice PDFs** — When enabled in Admin → Settings → Peppol e-Invoicing, exported invoice PDFs embed EN 16931 UBL XML as `ZUGFeRD-invoice.xml`, producing hybrid human- and machine-readable invoices. Uses the same UBL as Peppol; these PDFs can be sent via Peppol or email. New setting `invoices_zugferd_pdf`, migration `128_add_invoices_zugferd_pdf`, dependency `pikepdf`, and [docs/admin/configuration/PEPPOL_EINVOICING.md](docs/admin/configuration/PEPPOL_EINVOICING.md) updated for both Peppol and ZugFerd.
|
||||
- **Subcontractor role and assigned clients** — Users with the Subcontractor role can be restricted to specific clients and their projects. Admins assign clients in Admin → Users → Edit user (section "Assigned Clients (Subcontractor)"). Scope is applied to clients, projects, time entries, reports, invoices, timer, and API v1; direct access to other clients/projects returns 403. New table `user_clients`, migration `127_add_user_clients_table`, and docs in [docs/SUBCONTRACTOR_ROLE.md](docs/SUBCONTRACTOR_ROLE.md).
|
||||
|
||||
+131
-197
@@ -5531,17 +5531,19 @@ function initializePDFEditor() {
|
||||
|
||||
console.log('Decorative image dimensions:', actualWidth, 'x', actualHeight, 'URL:', imageUrl);
|
||||
|
||||
// Add to ReportLab template JSON
|
||||
templateJson.elements.push({
|
||||
type: 'image',
|
||||
x: pxToPt(x),
|
||||
y: pxToPt(y),
|
||||
width: pxToPt(actualWidth),
|
||||
height: pxToPt(actualHeight),
|
||||
source: imageUrl || '', // Store the image URL
|
||||
opacity: opacity,
|
||||
decorative: true // Mark as decorative image
|
||||
});
|
||||
// Issue #432: Only add to template JSON when source is non-empty (avoid empty decorative elements in PDF)
|
||||
if (imageUrl && imageUrl.trim() !== '') {
|
||||
templateJson.elements.push({
|
||||
type: 'image',
|
||||
x: pxToPt(x),
|
||||
y: pxToPt(y),
|
||||
width: pxToPt(actualWidth),
|
||||
height: pxToPt(actualHeight),
|
||||
source: imageUrl,
|
||||
opacity: opacity,
|
||||
decorative: true
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy HTML for preview - only generate image tag, not rectangle
|
||||
if (imageUrl && imageUrl.trim() !== '') {
|
||||
@@ -5752,19 +5754,21 @@ function initializePDFEditor() {
|
||||
|
||||
console.log('Decorative image dimensions (fallback):', actualWidth, 'x', actualHeight, 'URL:', imageUrl);
|
||||
|
||||
// Add to ReportLab template JSON
|
||||
templateJson.elements.push({
|
||||
type: 'image',
|
||||
x: pxToPt(x),
|
||||
y: pxToPt(y),
|
||||
width: pxToPt(actualWidth),
|
||||
height: pxToPt(actualHeight),
|
||||
source: imageUrl || '', // Store the image URL
|
||||
opacity: opacity,
|
||||
decorative: true // Mark as decorative image
|
||||
});
|
||||
// Issue #432: Only add to template JSON when source is non-empty
|
||||
if (imageUrl && imageUrl.trim() !== '') {
|
||||
templateJson.elements.push({
|
||||
type: 'image',
|
||||
x: pxToPt(x),
|
||||
y: pxToPt(y),
|
||||
width: pxToPt(actualWidth),
|
||||
height: pxToPt(actualHeight),
|
||||
source: imageUrl,
|
||||
opacity: opacity,
|
||||
decorative: true
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy HTML for preview - only generate image tag, not rectangle
|
||||
// Legacy HTML for preview
|
||||
if (imageUrl && imageUrl.trim() !== '') {
|
||||
bodyContent += ` <img src="${imageUrl}" style="position:absolute;left:${x}px;top:${y}px;width:${actualWidth}px;height:${actualHeight}px;opacity:${opacity}" alt="Decorative image" class="image-element">\n`;
|
||||
} else {
|
||||
@@ -6063,6 +6067,78 @@ table tr:last-child td {
|
||||
ensureTableGroupNames(layer, background);
|
||||
layer.draw();
|
||||
}
|
||||
// Issue #432: Sync imageUrl onto decorative-image groups BEFORE generateCode() so template_json has correct source
|
||||
layer.find('[name="decorative-image"]').forEach((decorativeImageGroup, idx) => {
|
||||
try {
|
||||
let imageUrl = decorativeImageGroup.getAttr('imageUrl');
|
||||
if (!imageUrl) imageUrl = decorativeImageGroup.attrs.imageUrl;
|
||||
if (!imageUrl) {
|
||||
const imageNode = decorativeImageGroup.findOne('Image');
|
||||
if (imageNode) {
|
||||
const imgAttrs = imageNode.attrs || {};
|
||||
imageUrl = imgAttrs.src || imgAttrs.url || imgAttrs.imageUrl || '';
|
||||
}
|
||||
}
|
||||
if (imageUrl) {
|
||||
decorativeImageGroup.setAttr('imageUrl', imageUrl);
|
||||
decorativeImageGroup.attrs.imageUrl = imageUrl;
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn(`[INVOICE] [SAVE] Could not get/set imageUrl for decorative image ${idx}:`, e);
|
||||
}
|
||||
});
|
||||
let allDecorativeGroups = layer.find('[name="decorative-image"]');
|
||||
layer.find('Group').forEach(group => {
|
||||
const name = group.name() || group.getAttr('name') || '';
|
||||
if (name.includes('decorative-image') && !allDecorativeGroups.includes(group)) {
|
||||
allDecorativeGroups = allDecorativeGroups.concat([group]);
|
||||
}
|
||||
});
|
||||
allDecorativeGroups.forEach((decorativeImageGroup, idx) => {
|
||||
try {
|
||||
const currentName = decorativeImageGroup.name() || decorativeImageGroup.getAttr('name') || '';
|
||||
if (!currentName.includes('decorative-image')) {
|
||||
decorativeImageGroup.setAttr('name', 'decorative-image');
|
||||
decorativeImageGroup.name('decorative-image');
|
||||
if (decorativeImageGroup.attrs) decorativeImageGroup.attrs.name = 'decorative-image';
|
||||
}
|
||||
const imageUrl = decorativeImageGroup.getAttr('imageUrl') || decorativeImageGroup.attrs.imageUrl;
|
||||
if (imageUrl) {
|
||||
decorativeImageGroup.setAttr('imageUrl', imageUrl);
|
||||
decorativeImageGroup.attrs.imageUrl = imageUrl;
|
||||
}
|
||||
} catch(e) {}
|
||||
});
|
||||
const decorativeImageUrlMap = new Map();
|
||||
let decorativeImageGroups = layer.find('[name="decorative-image"]');
|
||||
layer.find('Group').forEach(group => {
|
||||
const name = group.name() || group.getAttr('name') || '';
|
||||
if (name.includes('decorative-image') && !decorativeImageGroups.includes(group)) {
|
||||
decorativeImageGroups = decorativeImageGroups.concat([group]);
|
||||
}
|
||||
});
|
||||
decorativeImageGroups.forEach((decorativeImageGroup, index) => {
|
||||
const currentPrimaryName = decorativeImageGroup.name();
|
||||
if (currentPrimaryName !== 'decorative-image') {
|
||||
decorativeImageGroup.name('decorative-image');
|
||||
decorativeImageGroup.setAttr('name', 'decorative-image');
|
||||
if (decorativeImageGroup.attrs) decorativeImageGroup.attrs.name = 'decorative-image';
|
||||
}
|
||||
let imageUrl = decorativeImageGroup.getAttr('imageUrl') || decorativeImageGroup.attrs.imageUrl || '';
|
||||
if (!imageUrl) {
|
||||
const imageNode = decorativeImageGroup.findOne('Image');
|
||||
if (imageNode) {
|
||||
const imgAttrs = imageNode.attrs || {};
|
||||
imageUrl = imgAttrs.src || imgAttrs.url || imgAttrs.imageUrl || '';
|
||||
}
|
||||
}
|
||||
const x = decorativeImageGroup.x() || 0;
|
||||
const y = decorativeImageGroup.y() || 0;
|
||||
decorativeImageUrlMap.set(`${x}_${y}`, imageUrl || '');
|
||||
decorativeImageUrlMap.set(`${x}_${y}_${index}`, imageUrl || '');
|
||||
decorativeImageUrlMap.set(`index_${index}`, imageUrl || '');
|
||||
});
|
||||
|
||||
const { html, css, json } = generateCode();
|
||||
|
||||
// Log what we're saving for debugging
|
||||
@@ -6076,53 +6152,6 @@ table tr:last-child td {
|
||||
console.log('Number of elements:', layer.children.length);
|
||||
console.log('Page size:', CURRENT_PAGE_SIZE);
|
||||
|
||||
// Log all Groups to see if items-table is there
|
||||
layer.children.forEach((child, idx) => {
|
||||
if (child.className === 'Group') {
|
||||
console.log(`Group ${idx}: name="${child.attrs.name || 'unnamed'}"`, child.attrs);
|
||||
// Check for decorative images
|
||||
if (child.attrs.name === 'decorative-image') {
|
||||
const imageUrl = child.getAttr('imageUrl') || child.attrs.imageUrl || '';
|
||||
console.log(` Decorative image ${idx}: imageUrl="${imageUrl}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure imageUrl is in attrs for all decorative images before serialization
|
||||
layer.find('[name="decorative-image"]').forEach((decorativeImageGroup, idx) => {
|
||||
try {
|
||||
// Try multiple methods to get imageUrl
|
||||
let imageUrl = decorativeImageGroup.getAttr('imageUrl');
|
||||
console.log(`[INVOICE] [SAVE] Decorative image ${idx}: getAttr('imageUrl') =`, imageUrl);
|
||||
if (!imageUrl) {
|
||||
imageUrl = decorativeImageGroup.attrs.imageUrl;
|
||||
console.log(`[INVOICE] [SAVE] Decorative image ${idx}: attrs.imageUrl =`, imageUrl);
|
||||
}
|
||||
if (!imageUrl) {
|
||||
// Check if there's an Image node with the URL
|
||||
const imageNode = decorativeImageGroup.findOne('Image');
|
||||
if (imageNode) {
|
||||
// Try to get URL from image node's source if available
|
||||
const imgAttrs = imageNode.attrs || {};
|
||||
imageUrl = imgAttrs.src || imgAttrs.url || imgAttrs.imageUrl || '';
|
||||
console.log(`[INVOICE] [SAVE] Decorative image ${idx}: Found imageUrl in Image node:`, imageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
// Use setAttr to ensure Konva properly serializes it
|
||||
decorativeImageGroup.setAttr('imageUrl', imageUrl);
|
||||
// Also set in attrs for redundancy
|
||||
decorativeImageGroup.attrs.imageUrl = imageUrl;
|
||||
console.log(`[INVOICE] [SAVE] ✅ Ensured imageUrl in attrs for decorative image ${idx}:`, imageUrl);
|
||||
} else {
|
||||
console.warn(`[INVOICE] [SAVE] ⚠️ No imageUrl found for decorative image group ${idx}`);
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn(`[INVOICE] [SAVE] Could not get/set imageUrl for decorative image ${idx}:`, e);
|
||||
}
|
||||
});
|
||||
|
||||
// Save both legacy HTML/CSS and new JSON template
|
||||
// Ensure JSON is always present and valid
|
||||
if (!json || !json.trim()) {
|
||||
@@ -6140,99 +6169,6 @@ table tr:last-child td {
|
||||
return;
|
||||
}
|
||||
|
||||
// Final check: ensure all decorative images have imageUrl properly set before serialization
|
||||
// Also ensure base name "decorative-image" is preserved (even if "element-overlap" is added)
|
||||
let allDecorativeGroups = layer.find('[name="decorative-image"]');
|
||||
const allGroupsForCheck = layer.find('Group');
|
||||
allGroupsForCheck.forEach(group => {
|
||||
const name = group.name() || group.getAttr('name') || '';
|
||||
if (name.includes('decorative-image') && !allDecorativeGroups.includes(group)) {
|
||||
allDecorativeGroups = allDecorativeGroups.concat([group]);
|
||||
}
|
||||
});
|
||||
|
||||
allDecorativeGroups.forEach((decorativeImageGroup, idx) => {
|
||||
try {
|
||||
// Ensure base name is set (preserve "decorative-image" even if "element-overlap" exists)
|
||||
const currentName = decorativeImageGroup.name() || decorativeImageGroup.getAttr('name') || '';
|
||||
if (!currentName.includes('decorative-image')) {
|
||||
decorativeImageGroup.setAttr('name', 'decorative-image');
|
||||
decorativeImageGroup.name('decorative-image');
|
||||
if (decorativeImageGroup.attrs) {
|
||||
decorativeImageGroup.attrs.name = 'decorative-image';
|
||||
}
|
||||
console.log(`[INVOICE] [SAVE] Fixed name for decorative image ${idx}: was "${currentName}", now "decorative-image"`);
|
||||
}
|
||||
|
||||
// Double-check that imageUrl is set via both methods
|
||||
const imageUrl = decorativeImageGroup.getAttr('imageUrl') || decorativeImageGroup.attrs.imageUrl;
|
||||
if (imageUrl) {
|
||||
decorativeImageGroup.setAttr('imageUrl', imageUrl);
|
||||
decorativeImageGroup.attrs.imageUrl = imageUrl;
|
||||
console.log(`[INVOICE] [SAVE] Final check: imageUrl set for decorative image ${idx}:`, imageUrl);
|
||||
} else {
|
||||
console.warn(`[INVOICE] [SAVE] ⚠️ Final check: No imageUrl found for decorative image ${idx}`);
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn(`[INVOICE] [SAVE] Final check: Could not set imageUrl for decorative image ${idx}:`, e);
|
||||
}
|
||||
});
|
||||
|
||||
// CRITICAL FIX: Get imageUrl from actual nodes before serialization and store in a map
|
||||
// This ensures we can inject imageUrl into JSON after serialization
|
||||
const decorativeImageUrlMap = new Map();
|
||||
// Find all decorative-image groups, including those with modified names like "decorative-image element-overlap"
|
||||
let decorativeImageGroups = layer.find('[name="decorative-image"]');
|
||||
const allGroups = layer.find('Group');
|
||||
allGroups.forEach(group => {
|
||||
const name = group.name() || group.getAttr('name') || '';
|
||||
if (name.includes('decorative-image') && !decorativeImageGroups.includes(group)) {
|
||||
decorativeImageGroups = decorativeImageGroups.concat([group]);
|
||||
}
|
||||
});
|
||||
console.log('[INVOICE] [SAVE] Found', decorativeImageGroups.length, 'decorative image group(s) before serialization (including modified names)');
|
||||
decorativeImageGroups.forEach((decorativeImageGroup, index) => {
|
||||
// CRITICAL: Ensure primary name is "decorative-image" before serialization
|
||||
// Konva's toJSON() uses the primary name() method, not additional names from addName()
|
||||
const currentPrimaryName = decorativeImageGroup.name();
|
||||
if (currentPrimaryName !== 'decorative-image') {
|
||||
decorativeImageGroup.name('decorative-image');
|
||||
decorativeImageGroup.setAttr('name', 'decorative-image');
|
||||
if (decorativeImageGroup.attrs) {
|
||||
decorativeImageGroup.attrs.name = 'decorative-image';
|
||||
}
|
||||
console.log(`[INVOICE] [SAVE] Fixed primary name for decorative image ${index}: was "${currentPrimaryName}", now "decorative-image"`);
|
||||
}
|
||||
|
||||
// Try multiple methods to get imageUrl
|
||||
let imageUrl = decorativeImageGroup.getAttr('imageUrl') || '';
|
||||
if (!imageUrl) {
|
||||
imageUrl = decorativeImageGroup.attrs.imageUrl || '';
|
||||
}
|
||||
// Also check if there's an Image node with the URL stored in it
|
||||
if (!imageUrl) {
|
||||
const imageNode = decorativeImageGroup.findOne('Image');
|
||||
if (imageNode) {
|
||||
// Try to get URL from image node's source if available
|
||||
const imgAttrs = imageNode.attrs || {};
|
||||
imageUrl = imgAttrs.src || imgAttrs.url || imgAttrs.imageUrl || '';
|
||||
}
|
||||
}
|
||||
|
||||
const x = decorativeImageGroup.x() || 0;
|
||||
const y = decorativeImageGroup.y() || 0;
|
||||
// Use position as key to match nodes after serialization
|
||||
const key = `${x}_${y}_${index}`;
|
||||
decorativeImageUrlMap.set(key, imageUrl);
|
||||
// Also store by index as fallback
|
||||
decorativeImageUrlMap.set(`index_${index}`, imageUrl);
|
||||
console.log(`[INVOICE] [SAVE] Stored imageUrl for decorative image ${index} at (${x}, ${y}):`, imageUrl, 'key:', key);
|
||||
|
||||
if (!imageUrl) {
|
||||
console.warn(`[INVOICE] [SAVE] ⚠️ No imageUrl found for decorative image ${index} at (${x}, ${y})`);
|
||||
}
|
||||
});
|
||||
|
||||
// Serialize the stage
|
||||
const stageJson = stage.toJSON();
|
||||
|
||||
@@ -6258,45 +6194,20 @@ table tr:last-child td {
|
||||
}
|
||||
|
||||
// Manually inject imageUrl into serialized JSON for decorative images (backup pass)
|
||||
// Konva's toJSON() might not include custom attributes, so we need to add them manually
|
||||
// Prefer position-based matching so reordering does not swap URLs (Issue #432)
|
||||
let decorativeImageIndex = 0;
|
||||
function ensureImageUrlInJson(node, parentKey = '') {
|
||||
// CRITICAL FIX: Check if name includes 'decorative-image' (handles "decorative-image element-overlap")
|
||||
const nodeName = node.attrs && node.attrs.name ? node.attrs.name : '';
|
||||
if (nodeName && nodeName.includes('decorative-image')) {
|
||||
// Try to match by position
|
||||
const x = node.attrs.x || 0;
|
||||
const y = node.attrs.y || 0;
|
||||
const positionKey = `${x}_${y}_${decorativeImageIndex}`;
|
||||
const indexKey = `index_${decorativeImageIndex}`;
|
||||
|
||||
// Get imageUrl from map
|
||||
let imageUrl = decorativeImageUrlMap.get(positionKey) ||
|
||||
decorativeImageUrlMap.get(indexKey) || '';
|
||||
|
||||
// If still not found, try to find by matching all keys
|
||||
if (!imageUrl) {
|
||||
for (const [key, url] of decorativeImageUrlMap.entries()) {
|
||||
if (url) {
|
||||
imageUrl = url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
// Ensure imageUrl is in the JSON attrs
|
||||
if (!node.attrs) {
|
||||
node.attrs = {};
|
||||
}
|
||||
node.attrs.imageUrl = imageUrl;
|
||||
console.log(`[INVOICE] [SAVE] ✅ Injected imageUrl into JSON for decorative image ${decorativeImageIndex} at (${x}, ${y}):`, imageUrl);
|
||||
} else {
|
||||
console.warn(`[INVOICE] [SAVE] ⚠️ No imageUrl found for decorative image ${decorativeImageIndex} at position (${x}, ${y})`);
|
||||
// Debug: show what's in the map
|
||||
console.log(`[INVOICE] [SAVE] Map contents:`, Array.from(decorativeImageUrlMap.entries()));
|
||||
}
|
||||
|
||||
// Position-based first (no index), then with index, then index-only fallback
|
||||
let imageUrl = decorativeImageUrlMap.get(`${x}_${y}`) ||
|
||||
decorativeImageUrlMap.get(`${x}_${y}_${decorativeImageIndex}`) ||
|
||||
decorativeImageUrlMap.get(`index_${decorativeImageIndex}`) || '';
|
||||
if (!node.attrs) node.attrs = {};
|
||||
node.attrs.name = 'decorative-image';
|
||||
node.attrs.imageUrl = (typeof imageUrl === 'string' ? imageUrl : '');
|
||||
decorativeImageIndex++;
|
||||
}
|
||||
if (node.children) {
|
||||
@@ -6304,7 +6215,6 @@ table tr:last-child td {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure imageUrl is in JSON for all decorative images
|
||||
if (stageJson.children) {
|
||||
stageJson.children.forEach((child, idx) => ensureImageUrlInJson(child, `children[${idx}]`));
|
||||
}
|
||||
@@ -6557,6 +6467,30 @@ table tr:last-child td {
|
||||
stage = restoredStage;
|
||||
layer = stage.children[0];
|
||||
|
||||
// Issue #432: Synchronously restore name and imageUrl from saved JSON onto live nodes
|
||||
// (Konva may not restore custom attrs; this ensures they are set before any setTimeout)
|
||||
const layerJson = savedJson.children && savedJson.children[0];
|
||||
if (layerJson && layerJson.children && layer.children) {
|
||||
for (let i = 0; i < layer.children.length; i++) {
|
||||
const liveChild = layer.children[i];
|
||||
const jsonChild = layerJson.children[i];
|
||||
if (!liveChild || !jsonChild || liveChild.className !== 'Group') continue;
|
||||
const savedName = (jsonChild.attrs && jsonChild.attrs.name) ? jsonChild.attrs.name : '';
|
||||
if (savedName && savedName.includes('decorative-image')) {
|
||||
const savedImageUrl = (jsonChild.attrs && jsonChild.attrs.imageUrl) ? jsonChild.attrs.imageUrl : '';
|
||||
if (liveChild.setAttr) {
|
||||
liveChild.setAttr('name', 'decorative-image');
|
||||
liveChild.setAttr('imageUrl', savedImageUrl || '');
|
||||
}
|
||||
if (liveChild.attrs) {
|
||||
liveChild.attrs.name = 'decorative-image';
|
||||
liveChild.attrs.imageUrl = savedImageUrl || '';
|
||||
}
|
||||
if (liveChild.name) liveChild.name('decorative-image');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Log the saved JSON to check if imageUrl is there
|
||||
const savedJsonString = JSON.stringify(savedJson);
|
||||
console.log('[INVOICE] [LOAD] Saved JSON structure (first 2000 chars):', savedJsonString.substring(0, 2000));
|
||||
|
||||
@@ -5073,26 +5073,27 @@ function initializePDFEditor() {
|
||||
|
||||
console.log('Decorative image dimensions:', actualWidth, 'x', actualHeight, 'URL:', imageUrl);
|
||||
|
||||
// Add to ReportLab template JSON
|
||||
templateJson.elements.push({
|
||||
type: 'image',
|
||||
x: pxToPt(x),
|
||||
y: pxToPt(y),
|
||||
width: pxToPt(actualWidth),
|
||||
height: pxToPt(actualHeight),
|
||||
source: imageUrl || '', // Store the image URL
|
||||
opacity: opacity,
|
||||
decorative: true // Mark as decorative image
|
||||
});
|
||||
// Issue #432: Only add to template JSON when source is non-empty
|
||||
if (imageUrl && imageUrl.trim() !== '') {
|
||||
templateJson.elements.push({
|
||||
type: 'image',
|
||||
x: pxToPt(x),
|
||||
y: pxToPt(y),
|
||||
width: pxToPt(actualWidth),
|
||||
height: pxToPt(actualHeight),
|
||||
source: imageUrl,
|
||||
opacity: opacity,
|
||||
decorative: true
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy HTML for preview - only generate image tag, not rectangle
|
||||
// Legacy HTML for preview
|
||||
if (imageUrl && imageUrl.trim() !== '') {
|
||||
bodyContent += ` <img src="${imageUrl}" style="position:absolute;left:${x}px;top:${y}px;width:${actualWidth}px;height:${actualHeight}px;opacity:${opacity}" alt="Decorative image" class="image-element">\n`;
|
||||
} else {
|
||||
bodyContent += ` <div style="position:absolute;left:${x}px;top:${y}px;width:${actualWidth}px;height:${actualHeight}px;background:#f0f0f0;border:2px dashed #999;opacity:${opacity};display:flex;align-items:center;justify-content:center;color:#666;font-size:12px;">Decorative Image</div>\n`;
|
||||
}
|
||||
// Skip processing children of decorative-image groups - they are just placeholders
|
||||
return; // Use return instead of continue in forEach
|
||||
return;
|
||||
}
|
||||
|
||||
if (isItemsTable) {
|
||||
@@ -5292,19 +5293,21 @@ function initializePDFEditor() {
|
||||
|
||||
console.log('Decorative image dimensions (fallback):', actualWidth, 'x', actualHeight, 'URL:', imageUrl);
|
||||
|
||||
// Add to ReportLab template JSON
|
||||
templateJson.elements.push({
|
||||
type: 'image',
|
||||
x: pxToPt(x),
|
||||
y: pxToPt(y),
|
||||
width: pxToPt(actualWidth),
|
||||
height: pxToPt(actualHeight),
|
||||
source: imageUrl || '', // Store the image URL
|
||||
opacity: opacity,
|
||||
decorative: true // Mark as decorative image
|
||||
});
|
||||
// Issue #432: Only add to template JSON when source is non-empty
|
||||
if (imageUrl && imageUrl.trim() !== '') {
|
||||
templateJson.elements.push({
|
||||
type: 'image',
|
||||
x: pxToPt(x),
|
||||
y: pxToPt(y),
|
||||
width: pxToPt(actualWidth),
|
||||
height: pxToPt(actualHeight),
|
||||
source: imageUrl,
|
||||
opacity: opacity,
|
||||
decorative: true
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy HTML for preview - only generate image tag, not rectangle
|
||||
// Legacy HTML for preview
|
||||
if (imageUrl && imageUrl.trim() !== '') {
|
||||
bodyContent += ` <img src="${imageUrl}" style="position:absolute;left:${x}px;top:${y}px;width:${actualWidth}px;height:${actualHeight}px;opacity:${opacity}" alt="Decorative image" class="image-element">\n`;
|
||||
} else {
|
||||
@@ -5600,6 +5603,78 @@ table tr:last-child td {
|
||||
ensureQuoteTableGroupNames(layer, background);
|
||||
layer.draw();
|
||||
}
|
||||
// Issue #432: Sync imageUrl onto decorative-image groups BEFORE generateCode() so template_json has correct source
|
||||
layer.find('[name="decorative-image"]').forEach((decorativeImageGroup, idx) => {
|
||||
try {
|
||||
let imageUrl = decorativeImageGroup.getAttr('imageUrl');
|
||||
if (!imageUrl) imageUrl = decorativeImageGroup.attrs.imageUrl;
|
||||
if (!imageUrl) {
|
||||
const imageNode = decorativeImageGroup.findOne('Image');
|
||||
if (imageNode) {
|
||||
const imgAttrs = imageNode.attrs || {};
|
||||
imageUrl = imgAttrs.src || imgAttrs.url || imgAttrs.imageUrl || '';
|
||||
}
|
||||
}
|
||||
if (imageUrl) {
|
||||
decorativeImageGroup.setAttr('imageUrl', imageUrl);
|
||||
decorativeImageGroup.attrs.imageUrl = imageUrl;
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn('Could not get/set imageUrl for decorative image:', e);
|
||||
}
|
||||
});
|
||||
let allDecorativeGroups = layer.find('[name="decorative-image"]');
|
||||
layer.find('Group').forEach(group => {
|
||||
const name = group.name() || group.getAttr('name') || '';
|
||||
if (name.includes('decorative-image') && !allDecorativeGroups.includes(group)) {
|
||||
allDecorativeGroups = allDecorativeGroups.concat([group]);
|
||||
}
|
||||
});
|
||||
allDecorativeGroups.forEach((decorativeImageGroup, idx) => {
|
||||
try {
|
||||
const currentName = decorativeImageGroup.name() || decorativeImageGroup.getAttr('name') || '';
|
||||
if (!currentName.includes('decorative-image')) {
|
||||
decorativeImageGroup.setAttr('name', 'decorative-image');
|
||||
decorativeImageGroup.name('decorative-image');
|
||||
if (decorativeImageGroup.attrs) decorativeImageGroup.attrs.name = 'decorative-image';
|
||||
}
|
||||
const imageUrl = decorativeImageGroup.getAttr('imageUrl') || decorativeImageGroup.attrs.imageUrl;
|
||||
if (imageUrl) {
|
||||
decorativeImageGroup.setAttr('imageUrl', imageUrl);
|
||||
decorativeImageGroup.attrs.imageUrl = imageUrl;
|
||||
}
|
||||
} catch(e) {}
|
||||
});
|
||||
const decorativeImageUrlMap = new Map();
|
||||
let decorativeImageGroups = layer.find('[name="decorative-image"]');
|
||||
layer.find('Group').forEach(group => {
|
||||
const name = group.name() || group.getAttr('name') || '';
|
||||
if (name.includes('decorative-image') && !decorativeImageGroups.includes(group)) {
|
||||
decorativeImageGroups = decorativeImageGroups.concat([group]);
|
||||
}
|
||||
});
|
||||
decorativeImageGroups.forEach((decorativeImageGroup, index) => {
|
||||
const currentPrimaryName = decorativeImageGroup.name();
|
||||
if (currentPrimaryName !== 'decorative-image') {
|
||||
decorativeImageGroup.name('decorative-image');
|
||||
decorativeImageGroup.setAttr('name', 'decorative-image');
|
||||
if (decorativeImageGroup.attrs) decorativeImageGroup.attrs.name = 'decorative-image';
|
||||
}
|
||||
let imageUrl = decorativeImageGroup.getAttr('imageUrl') || decorativeImageGroup.attrs.imageUrl || '';
|
||||
if (!imageUrl) {
|
||||
const imageNode = decorativeImageGroup.findOne('Image');
|
||||
if (imageNode) {
|
||||
const imgAttrs = imageNode.attrs || {};
|
||||
imageUrl = imgAttrs.src || imgAttrs.url || imgAttrs.imageUrl || '';
|
||||
}
|
||||
}
|
||||
const x = decorativeImageGroup.x() || 0;
|
||||
const y = decorativeImageGroup.y() || 0;
|
||||
decorativeImageUrlMap.set(`${x}_${y}`, imageUrl || '');
|
||||
decorativeImageUrlMap.set(`${x}_${y}_${index}`, imageUrl || '');
|
||||
decorativeImageUrlMap.set(`index_${index}`, imageUrl || '');
|
||||
});
|
||||
|
||||
const { html, css, json } = generateCode();
|
||||
|
||||
// Log what we're saving for debugging
|
||||
@@ -5610,83 +5685,10 @@ table tr:last-child td {
|
||||
console.log('Number of elements:', layer.children.length);
|
||||
console.log('Page size:', CURRENT_PAGE_SIZE);
|
||||
|
||||
// Log all Groups to see if items-table is there
|
||||
layer.children.forEach((child, idx) => {
|
||||
if (child.className === 'Group') {
|
||||
console.log(`Group ${idx}: name="${child.attrs.name || 'unnamed'}"`, child.attrs);
|
||||
// Check for decorative images
|
||||
if (child.attrs.name === 'decorative-image') {
|
||||
const imageUrl = child.getAttr('imageUrl') || child.attrs.imageUrl || '';
|
||||
console.log(` Decorative image ${idx}: imageUrl="${imageUrl}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure imageUrl is in attrs for all decorative images before serialization
|
||||
layer.find('[name="decorative-image"]').forEach(decorativeImageGroup => {
|
||||
try {
|
||||
// Try multiple methods to get imageUrl
|
||||
let imageUrl = decorativeImageGroup.getAttr('imageUrl');
|
||||
if (!imageUrl) {
|
||||
imageUrl = decorativeImageGroup.attrs.imageUrl;
|
||||
}
|
||||
if (!imageUrl) {
|
||||
// Check if there's an Image node with the URL
|
||||
const imageNode = decorativeImageGroup.findOne('Image');
|
||||
if (imageNode) {
|
||||
// Try to get URL from image node's source if available
|
||||
const imgAttrs = imageNode.attrs || {};
|
||||
imageUrl = imgAttrs.src || imgAttrs.url || imgAttrs.imageUrl || '';
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
// Use setAttr to ensure Konva properly serializes it
|
||||
decorativeImageGroup.setAttr('imageUrl', imageUrl);
|
||||
// Also set in attrs for redundancy
|
||||
decorativeImageGroup.attrs.imageUrl = imageUrl;
|
||||
console.log('Ensured imageUrl in attrs for decorative image:', imageUrl);
|
||||
} else {
|
||||
console.warn('No imageUrl found for decorative image group');
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn('Could not get/set imageUrl for decorative image:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// Final check: ensure all decorative images have imageUrl properly set before serialization
|
||||
layer.find('[name="decorative-image"]').forEach(decorativeImageGroup => {
|
||||
try {
|
||||
// Double-check that imageUrl is set via both methods
|
||||
const imageUrl = decorativeImageGroup.getAttr('imageUrl') || decorativeImageGroup.attrs.imageUrl;
|
||||
if (imageUrl) {
|
||||
decorativeImageGroup.setAttr('imageUrl', imageUrl);
|
||||
decorativeImageGroup.attrs.imageUrl = imageUrl;
|
||||
console.log('Final check: imageUrl set for decorative image:', imageUrl);
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn('Final check: Could not set imageUrl:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// CRITICAL FIX: Get imageUrl from actual nodes before serialization and store in a map
|
||||
// This ensures we can inject imageUrl into JSON after serialization
|
||||
const decorativeImageUrlMap = new Map();
|
||||
layer.find('[name="decorative-image"]').forEach((decorativeImageGroup, index) => {
|
||||
let imageUrl = decorativeImageGroup.getAttr('imageUrl') || decorativeImageGroup.attrs.imageUrl || '';
|
||||
// Use position as key to match nodes after serialization
|
||||
const key = `${decorativeImageGroup.x()}_${decorativeImageGroup.y()}_${index}`;
|
||||
decorativeImageUrlMap.set(key, imageUrl);
|
||||
// Also store by index as fallback
|
||||
decorativeImageUrlMap.set(`index_${index}`, imageUrl);
|
||||
console.log(`Stored imageUrl for decorative image ${index}:`, imageUrl, 'key:', key);
|
||||
});
|
||||
|
||||
// Serialize the stage
|
||||
const stageJson = stage.toJSON();
|
||||
|
||||
// CRITICAL FIX: Explicitly inject name and imageUrl into serialized JSON for decorative images
|
||||
// by matching live layer children to JSON by index (Konva may not serialize custom attrs)
|
||||
// Explicitly inject name and imageUrl into serialized JSON (Issue #432)
|
||||
const layerJson = stageJson.children && stageJson.children[0];
|
||||
if (layer && layerJson && layerJson.children && layer.children) {
|
||||
for (let i = 0; i < layer.children.length; i++) {
|
||||
@@ -5705,42 +5707,18 @@ table tr:last-child td {
|
||||
}
|
||||
}
|
||||
|
||||
// Manually inject imageUrl into serialized JSON for decorative images (backup pass)
|
||||
// Konva's toJSON() might not include custom attributes, so we need to add them manually
|
||||
let decorativeImageIndex = 0;
|
||||
function ensureImageUrlInJson(node, parentKey = '') {
|
||||
if (node.attrs && node.attrs.name === 'decorative-image') {
|
||||
// Try to match by position
|
||||
const nodeName = node.attrs && node.attrs.name ? node.attrs.name : '';
|
||||
if (nodeName && nodeName.includes('decorative-image')) {
|
||||
const x = node.attrs.x || 0;
|
||||
const y = node.attrs.y || 0;
|
||||
const positionKey = `${x}_${y}_${decorativeImageIndex}`;
|
||||
const indexKey = `index_${decorativeImageIndex}`;
|
||||
|
||||
// Get imageUrl from map
|
||||
let imageUrl = decorativeImageUrlMap.get(positionKey) ||
|
||||
decorativeImageUrlMap.get(indexKey) || '';
|
||||
|
||||
// If still not found, try to find by matching all keys
|
||||
if (!imageUrl) {
|
||||
for (const [key, url] of decorativeImageUrlMap.entries()) {
|
||||
if (url) {
|
||||
imageUrl = url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
// Ensure imageUrl is in the JSON attrs
|
||||
if (!node.attrs) {
|
||||
node.attrs = {};
|
||||
}
|
||||
node.attrs.imageUrl = imageUrl;
|
||||
console.log(`Injected imageUrl into JSON for decorative image ${decorativeImageIndex}:`, imageUrl);
|
||||
} else {
|
||||
console.warn(`No imageUrl found for decorative image ${decorativeImageIndex} at position (${x}, ${y})`);
|
||||
}
|
||||
|
||||
let imageUrl = decorativeImageUrlMap.get(`${x}_${y}`) ||
|
||||
decorativeImageUrlMap.get(`${x}_${y}_${decorativeImageIndex}`) ||
|
||||
decorativeImageUrlMap.get(`index_${decorativeImageIndex}`) || '';
|
||||
if (!node.attrs) node.attrs = {};
|
||||
node.attrs.name = 'decorative-image';
|
||||
node.attrs.imageUrl = (typeof imageUrl === 'string' ? imageUrl : '');
|
||||
decorativeImageIndex++;
|
||||
}
|
||||
if (node.children) {
|
||||
@@ -5748,34 +5726,18 @@ table tr:last-child td {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure imageUrl is in JSON for all decorative images
|
||||
if (stageJson.children) {
|
||||
stageJson.children.forEach((child, idx) => ensureImageUrlInJson(child, `children[${idx}]`));
|
||||
}
|
||||
|
||||
// Debug: Log decorative image groups in the JSON
|
||||
function logDecorativeImagesInJson(node, depth = 0) {
|
||||
if (node.attrs && node.attrs.name === 'decorative-image') {
|
||||
console.log(' '.repeat(depth) + 'Found decorative-image in JSON, imageUrl:', node.attrs.imageUrl || 'NOT FOUND');
|
||||
}
|
||||
if (node.children) {
|
||||
node.children.forEach(child => logDecorativeImagesInJson(child, depth + 1));
|
||||
}
|
||||
}
|
||||
console.log('=== Checking decorative images in serialized JSON ===');
|
||||
logDecorativeImagesInJson(stageJson);
|
||||
|
||||
document.getElementById('save-html').value = html;
|
||||
document.getElementById('save-css').value = css;
|
||||
document.getElementById('save-design-json').value = JSON.stringify(stageJson);
|
||||
// Ensure JSON is always present and valid
|
||||
if (!json || !json.trim()) {
|
||||
console.error('No JSON generated from template!');
|
||||
alert('Error: Could not generate template JSON. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate JSON before saving
|
||||
try {
|
||||
JSON.parse(json);
|
||||
} catch (e) {
|
||||
@@ -5783,7 +5745,6 @@ table tr:last-child td {
|
||||
alert('Error: Generated template JSON is invalid. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('save-template-json').value = json;
|
||||
// Use page size from selector if available, otherwise use CURRENT_PAGE_SIZE
|
||||
const pageSizeForSave = (pageSizeSelector && pageSizeSelector.value) || CURRENT_PAGE_SIZE || 'A4';
|
||||
@@ -6020,8 +5981,28 @@ table tr:last-child td {
|
||||
stage = restoredStage;
|
||||
layer = stage.children[0];
|
||||
|
||||
// Debug: Log the saved JSON to check if imageUrl is there
|
||||
console.log('Saved JSON structure (first 2000 chars):', JSON.stringify(savedJson).substring(0, 2000));
|
||||
// Issue #432: Synchronously restore name and imageUrl from saved JSON onto live nodes
|
||||
const layerJson = savedJson.children && savedJson.children[0];
|
||||
if (layerJson && layerJson.children && layer.children) {
|
||||
for (let i = 0; i < layer.children.length; i++) {
|
||||
const liveChild = layer.children[i];
|
||||
const jsonChild = layerJson.children[i];
|
||||
if (!liveChild || !jsonChild || liveChild.className !== 'Group') continue;
|
||||
const savedName = (jsonChild.attrs && jsonChild.attrs.name) ? jsonChild.attrs.name : '';
|
||||
if (savedName && savedName.includes('decorative-image')) {
|
||||
const savedImageUrl = (jsonChild.attrs && jsonChild.attrs.imageUrl) ? jsonChild.attrs.imageUrl : '';
|
||||
if (liveChild.setAttr) {
|
||||
liveChild.setAttr('name', 'decorative-image');
|
||||
liveChild.setAttr('imageUrl', savedImageUrl || '');
|
||||
}
|
||||
if (liveChild.attrs) {
|
||||
liveChild.attrs.name = 'decorative-image';
|
||||
liveChild.attrs.imageUrl = savedImageUrl || '';
|
||||
}
|
||||
if (liveChild.name) liveChild.name('decorative-image');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find or create background by name and resize it
|
||||
background = layer.findOne('[name="background"]');
|
||||
|
||||
@@ -429,29 +429,27 @@ class ReportLabTemplateRenderer:
|
||||
# Process template variables for source
|
||||
source = self._process_template_variables(source)
|
||||
|
||||
# CRITICAL FIX: Skip decorative images with empty or invalid source
|
||||
# Decorative images are optional and should not break PDF generation if missing
|
||||
if not source or not source.strip():
|
||||
# For decorative images, silently skip if source is empty
|
||||
if element.get("decorative", False):
|
||||
if current_app:
|
||||
current_app.logger.warning(f"Skipping decorative image flowable with empty source")
|
||||
return None
|
||||
# For non-decorative images, also return None
|
||||
# Issue #432: Explicit skip for decorative images with empty source (never add flowable or draw)
|
||||
if element.get("decorative", False) and (not source or not source.strip()):
|
||||
if current_app:
|
||||
current_app.logger.warning("Skipping decorative image flowable with empty source")
|
||||
return None
|
||||
|
||||
# Handle base64 data URI
|
||||
if not source or not source.strip():
|
||||
return None
|
||||
|
||||
# Handle base64 data URI with validated decode (Issue #432)
|
||||
if source.startswith("data:image"):
|
||||
# Extract base64 data
|
||||
import base64
|
||||
header, data = source.split(",", 1)
|
||||
parts = source.split(",", 1)
|
||||
if len(parts) < 2 or not (parts[1] and parts[1].strip()):
|
||||
if current_app:
|
||||
current_app.logger.warning("Skipping image: data URI has no base64 payload")
|
||||
return None
|
||||
try:
|
||||
img_data = base64.b64decode(data)
|
||||
img_data = base64.b64decode(parts[1])
|
||||
img_reader = ImageReader(io.BytesIO(img_data))
|
||||
|
||||
width = element.get("width", 100) # Already in points from generateCode
|
||||
height = element.get("height", 100) # Already in points from generateCode
|
||||
|
||||
width = element.get("width", 100)
|
||||
height = element.get("height", 100)
|
||||
return Image(img_reader, width=width, height=height)
|
||||
except Exception as e:
|
||||
if current_app:
|
||||
@@ -1026,19 +1024,16 @@ class ReportLabTemplateRenderer:
|
||||
source = element.get("source", "")
|
||||
source = self._process_template_variables(source)
|
||||
|
||||
# CRITICAL FIX: Skip decorative images with empty or invalid source to prevent black screen
|
||||
# Decorative images are optional and should not break PDF generation if missing
|
||||
# Issue #432: Explicit skip for decorative images with empty source (never draw)
|
||||
if element.get("decorative", False) and (not source or not source.strip()):
|
||||
if current_app:
|
||||
current_app.logger.warning(f"Skipping decorative image with empty source at position ({x}, {y})")
|
||||
return
|
||||
if not source or not source.strip():
|
||||
# For decorative images, silently skip if source is empty
|
||||
if element.get("decorative", False):
|
||||
if current_app:
|
||||
current_app.logger.warning(f"Skipping decorative image with empty source at position ({x}, {y})")
|
||||
return
|
||||
# For non-decorative images, also skip but log warning
|
||||
if current_app:
|
||||
current_app.logger.warning(f"Skipping image with empty source at position ({x}, {y})")
|
||||
return
|
||||
|
||||
|
||||
width = element.get("width", 100)
|
||||
height = element.get("height", 100)
|
||||
|
||||
@@ -1058,14 +1053,22 @@ class ReportLabTemplateRenderer:
|
||||
return
|
||||
|
||||
try:
|
||||
# Handle base64 data URI
|
||||
# Handle base64 data URI with validated decode (Issue #432)
|
||||
if source.startswith("data:image"):
|
||||
import base64
|
||||
header, data = source.split(",", 1)
|
||||
img_data = base64.b64decode(data)
|
||||
img_reader = ImageReader(io.BytesIO(img_data))
|
||||
# mask='auto' preserves transparency for PNG images
|
||||
canv.drawImage(img_reader, x, y - height, width=width, height=height, preserveAspectRatio=True, mask='auto')
|
||||
parts = source.split(",", 1)
|
||||
if len(parts) < 2 or not (parts[1] and parts[1].strip()):
|
||||
if current_app:
|
||||
current_app.logger.warning("Skipping image draw: data URI has no base64 payload")
|
||||
return
|
||||
try:
|
||||
img_data = base64.b64decode(parts[1])
|
||||
img_reader = ImageReader(io.BytesIO(img_data))
|
||||
canv.drawImage(img_reader, x, y - height, width=width, height=height, preserveAspectRatio=True, mask='auto')
|
||||
except Exception as e:
|
||||
if current_app:
|
||||
current_app.logger.error(f"Error decoding base64 image for canvas: {e}")
|
||||
return
|
||||
# Handle template image URLs (convert to file path or base64)
|
||||
elif source.startswith("/uploads/template_images/"):
|
||||
try:
|
||||
|
||||
@@ -16,6 +16,7 @@ The editor now includes a comprehensive set of draggable elements organized into
|
||||
- **Line**: Horizontal divider line
|
||||
- **Rectangle**: Customizable rectangular shape
|
||||
- **Circle**: Customizable circular shape
|
||||
- **Decorative Image**: Placeholder that you can assign an uploaded image to; appears in the PDF when an image is set. Supports position, size, and opacity. Save the layout after uploading to persist the image with the design.
|
||||
|
||||
#### Company Information Elements
|
||||
- **Company Logo**: Displays uploaded company logo
|
||||
@@ -152,9 +153,11 @@ The editor generates clean HTML and CSS:
|
||||
### State Persistence
|
||||
|
||||
Designs are saved as:
|
||||
1. **JSON**: Complete Konva.js stage state (for editing)
|
||||
2. **HTML**: Generated template markup
|
||||
3. **CSS**: Corresponding styles
|
||||
1. **design_json**: Complete Konva.js stage state (for re-opening the editor). Custom attributes such as decorative-image `name` and `imageUrl` are injected into the serialized JSON so they survive save/load.
|
||||
2. **template_json**: ReportLab template (elements list) used for PDF generation. Decorative image elements are only included when they have a non-empty image source, so the PDF is never broken by missing images.
|
||||
3. **HTML/CSS**: Generated template markup and styles (legacy preview).
|
||||
|
||||
**Decorative images:** The editor syncs each decorative image’s `imageUrl` onto its group before generating the template and uses position-based matching when injecting URLs into the saved design JSON. On load, `name` and `imageUrl` are restored from the saved JSON onto the live nodes so decorative images appear correctly even if Konva does not persist custom attributes. Placeholders (no image uploaded) remain visible in the editor but are omitted from the PDF.
|
||||
|
||||
## Usage Guide
|
||||
|
||||
@@ -238,6 +241,9 @@ Add custom properties by:
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Decorative Image Missing After Save
|
||||
If a decorative image disappears when you reopen the layout, ensure you clicked **Save Design** after uploading the image. The editor persists decorative images by syncing the image URL to the design and template before save; if you leave before the save completes or before uploading an image, the placeholder may not be stored correctly. If the PDF preview shows a black or wrong area, the same fix applies: save the layout after assigning the image.
|
||||
|
||||
### Element Not Appearing
|
||||
- Check console for errors
|
||||
- Verify element type in templates object
|
||||
|
||||
@@ -31,7 +31,7 @@ To access the PDF layout editor:
|
||||
The PDF Layout Editor is powered by **Konva.js** and consists of three main sections:
|
||||
|
||||
1. **Element Library (Left Sidebar)**: Drag-and-drop elements organized by category
|
||||
- Basic Elements (text, shapes, lines)
|
||||
- Basic Elements (text, shapes, lines, decorative images)
|
||||
- Company Information (logo, name, address, contact details)
|
||||
- Invoice Data (numbers, dates, client info, totals)
|
||||
- Advanced Elements (QR codes, watermarks, page numbers)
|
||||
|
||||
Reference in New Issue
Block a user