feat: add environment controls, API URL display, and improved auto-sizing

Features:
- Add ENABLE_UI and ENABLE_API environment variables for deployment flexibility
- Add API URL display textbox with copy button in web interface
- Create /api/calculate-auto-values endpoint to centralize sizing logic
- Dimension overlay now calls API for accurate calculations instead of duplicating logic

Improvements:
- Enhance auto-sizing algorithm with aspect-ratio awareness
- Improve padding calculation using geometric mean formula
- Font detection now uses all available fonts from get_available_fonts()
- API URL includes all parameters (font_path, font_size, padding, etc.)

UI/UX:
- Remove hidden header section and logo copy buttons
- API URL textarea wraps and grows vertically to show full URL
- Clean up unused code and debug console.log statements

Dependencies:
- Add gunicorn==21.2.0 to requirements.txt

Documentation:
- Document environment variables with Docker and Python examples
- Add production deployment guidance
- Update API documentation with font_path parameter
- Add API URL feature to usage instructions

Bug fixes:
- Change debug=False for production
- Remove unused 're' import from app.py
This commit is contained in:
2026-01-17 13:48:15 -05:00
parent 954cd87a20
commit d26f619901
4 changed files with 515 additions and 251 deletions

View File

@@ -161,10 +161,6 @@
}
}
.header {
display: none;
}
.content {
padding: 40px;
}
@@ -678,30 +674,6 @@
text-overflow: ellipsis;
padding: 0 6px;
}
.logo-copy-icon {
position: absolute;
bottom: 34px;
right: 6px;
width: 28px;
height: 28px;
background: rgba(42, 42, 42, 0.9);
border: 1px solid #444;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
z-index: 10;
}
.logo-copy-icon:hover {
background: rgba(102, 126, 234, 0.95);
border-color: #667eea;
transform: scale(1.1);
}
.loading-spinner {
text-align: center;
@@ -726,11 +698,6 @@
</head>
<body>
<div class="container">
<div class="header">
<h1>📺 Logo Text Adder</h1>
<p>Add custom text to your TV station and network logos</p>
</div>
<div class="main-layout">
<div class="form-container">
<div class="content">
@@ -893,6 +860,11 @@
Show dimensions on preview
</label>
<div class="preview-download-hint">Click image to download</div>
<div style="margin-top: 20px; max-width: 800px; margin-left: auto; margin-right: auto;">
<label for="apiUrl" style="display: block; margin-bottom: 8px; color: #e0e0e0; font-weight: 600; font-size: 0.95em;">API Call URL:</label>
<textarea id="apiUrl" readonly style="width: 100%; padding: 12px 15px; border: 2px solid #333; border-radius: 8px; font-size: 14px; background: #2a2a2a; color: #e0e0e0; font-family: monospace; cursor: text; user-select: all; resize: vertical; min-height: 44px; line-height: 1.5; overflow-y: auto; white-space: pre-wrap; word-break: break-all;" onclick="this.select()"></textarea>
<small style="display: block; margin-top: 6px; color: #666; font-size: 0.85em;">Use this URL to programmatically generate the same logo via the API</small>
</div>
</div>
</div>
</div>
@@ -966,6 +938,8 @@
// Trigger live preview when font changes
triggerLivePreview();
// Update API URL when font changes
updateApiUrl();
}
// Load available fonts
@@ -1071,7 +1045,6 @@
}
allLogos = data.logos;
console.log(`Loaded ${data.total || allLogos.length} total logos`);
// Populate country filter if not already done
if (allCountries.length === 0 && data.countries) {
@@ -1114,24 +1087,9 @@
logoItem.className = 'logo-item';
logoItem.innerHTML = `
<img src="${logo.thumbnail}" alt="${logo.name}" loading="lazy">
<div class="logo-copy-icon" data-url="${logo.url}" title="Copy direct link to image">📋</div>
<div class="logo-item-name" title="${logo.name} (${logo.country})">${logo.name}</div>
`;
// Add click handler for copy icon
const copyIcon = logoItem.querySelector('.logo-copy-icon');
copyIcon.addEventListener('click', (e) => {
e.stopPropagation();
const url = e.target.dataset.url;
navigator.clipboard.writeText(url).then(() => {
const originalText = e.target.textContent;
e.target.textContent = '✅';
setTimeout(() => {
e.target.textContent = originalText;
}, 2000);
});
});
logoItem.addEventListener('click', () => {
// Remove selected class from all items
document.querySelectorAll('.logo-item').forEach(item => {
@@ -1184,24 +1142,9 @@
logoItem.className = 'logo-item';
logoItem.innerHTML = `
<img src="${logo.thumbnail}" alt="${logo.name}" loading="lazy">
<div class="logo-copy-icon" data-url="${logo.url}" title="Copy direct link to image">📋</div>
<div class="logo-item-name" title="${logo.name} (${logo.country})">${logo.name}</div>
`;
// Add click handler for copy icon
const copyIcon = logoItem.querySelector('.logo-copy-icon');
copyIcon.addEventListener('click', (e) => {
e.stopPropagation();
const url = e.target.dataset.url;
navigator.clipboard.writeText(url).then(() => {
const originalText = e.target.textContent;
e.target.textContent = '✅';
setTimeout(() => {
e.target.textContent = originalText;
}, 2000);
});
});
logoItem.addEventListener('click', () => {
document.querySelectorAll('.logo-item').forEach(item => {
item.classList.remove('selected');
@@ -1424,166 +1367,185 @@
const naturalHeight = previewImage.naturalHeight;
const scale = imgWidth / naturalWidth;
// Parse actual values (could be 'auto' or a number)
// For left/right: auto font size is 20% of height, for above/below: 12% of width
let fontSizeNum;
if (fontSize === 'auto') {
if (position === 'left' || position === 'right') {
fontSizeNum = Math.round(originalHeight * 0.20);
// Use API to get calculated values if auto, otherwise use provided values
const updateVisualization = async () => {
let fontSizeNum, paddingNum;
if (fontSize === 'auto' || padding === 'auto') {
try {
const params = new URLSearchParams({
width: originalWidth,
height: originalHeight,
position: position,
font_size: fontSize,
padding: padding
});
const response = await fetch(`/api/calculate-auto-values?${params}`);
const data = await response.json();
fontSizeNum = fontSize === 'auto' ? data.font_size : parseInt(fontSize);
paddingNum = padding === 'auto' ? data.padding : parseInt(padding);
} catch (error) {
console.error('Failed to calculate auto values:', error);
// Fallback to manual values if API fails
fontSizeNum = parseInt(fontSize) || 60;
paddingNum = parseInt(padding) || 20;
}
} else {
fontSizeNum = Math.round(originalWidth * 0.12);
fontSizeNum = parseInt(fontSize);
paddingNum = parseInt(padding);
}
} else {
fontSizeNum = parseInt(fontSize);
}
const paddingNum = padding === 'auto' ? Math.round(fontSizeNum * 0.25) : parseInt(padding);
fontSizeLabel.textContent = `Font: ${fontSize === 'auto' ? `~${fontSizeNum} (auto)` : fontSize}px`;
paddingLabel.textContent = `Padding: ${padding === 'auto' ? `~${paddingNum} (auto)` : padding}px`;
// Backend uses inner_padding (full padding) and outer_padding (padding/4)
const innerPadding = paddingNum * scale;
const outerPadding = (paddingNum / 4) * scale;
// For horizontal text (left/right positions), we need to estimate actual text width
// Use canvas to measure text width more accurately
const text = document.getElementById('text').value;
let textWidth, textHeight;
if (position === 'left' || position === 'right') {
// For left/right: use actual text width and actual bounding box height
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = `${fontSizeNum}px Arial`;
const metrics = ctx.measureText(text);
textWidth = metrics.width * scale;
// Use actual text bounding box height (tighter than font size)
textHeight = fontSizeNum * 0.75 * scale;
} else {
// For above/below: text height is tighter bounding box
textHeight = fontSizeNum * 0.75 * scale;
textWidth = fontSizeNum * scale; // Not used in above/below
}
// Calculate text_area dimensions (matches backend exactly)
let textAreaHeight, textAreaWidth;
if (position === 'below' || position === 'above') {
textAreaHeight = textHeight + outerPadding + innerPadding;
} else {
textAreaWidth = textWidth + outerPadding + innerPadding;
}
// Calculate where the original logo is positioned in the final image
const scaledOriginalWidth = originalWidth * scale;
const scaledOriginalHeight = originalHeight * scale;
// Reset all styles completely
fontSizeIndicator.style.cssText = 'position: absolute; border: 2px dashed #667eea; background: rgba(102, 126, 234, 0.1); display: block;';
paddingIndicator.style.cssText = 'position: absolute; border: 2px dashed #4caf50; background: rgba(76, 175, 80, 0.1); display: block;';
fontSizeLabel.style.cssText = 'position: absolute; background: #667eea; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;';
paddingLabel.style.cssText = 'position: absolute; background: #4caf50; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;';
if (position === 'below') {
// Layout: [logo at y=0] [innerPadding] [text] [outerPadding]
// Blue box: shows ONLY the text bounding box - FULL WIDTH
fontSizeIndicator.style.left = '0px';
fontSizeIndicator.style.right = '0px';
fontSizeIndicator.style.top = `${scaledOriginalHeight + innerPadding}px`;
fontSizeIndicator.style.height = `${textHeight}px`;
// Font label on the LEFT side, anchored to BOTTOM edge of text box
fontSizeLabel.style.right = '100%';
fontSizeLabel.style.bottom = '0px';
fontSizeLabel.style.marginRight = '8px';
// Green box: shows ONLY innerPadding - FULL WIDTH
paddingIndicator.style.left = '0px';
paddingIndicator.style.right = '0px';
paddingIndicator.style.top = `${scaledOriginalHeight}px`;
paddingIndicator.style.height = `${innerPadding}px`;
// Padding label on the RIGHT side, anchored to BOTTOM edge of padding box
paddingLabel.style.left = '100%';
paddingLabel.style.bottom = '0px';
paddingLabel.style.marginLeft = '8px';
} else if (position === 'above') {
// Layout: [outerPadding] [text] [innerPadding] [logo]
// Blue box: shows ONLY the text bounding box - FULL WIDTH
fontSizeIndicator.style.left = '0px';
fontSizeIndicator.style.right = '0px';
fontSizeIndicator.style.top = `${outerPadding}px`;
fontSizeIndicator.style.height = `${textHeight}px`;
// Font label on the LEFT side, anchored to TOP edge of text box
fontSizeLabel.style.right = '100%';
fontSizeLabel.style.top = '0px';
fontSizeLabel.style.marginRight = '8px';
// Green box: shows ONLY innerPadding - FULL WIDTH
paddingIndicator.style.left = '0px';
paddingIndicator.style.right = '0px';
paddingIndicator.style.top = `${outerPadding + textHeight}px`;
paddingIndicator.style.height = `${innerPadding}px`;
// Padding label on the RIGHT side, anchored to TOP edge of padding box
paddingLabel.style.left = '100%';
paddingLabel.style.top = '0px';
paddingLabel.style.marginLeft = '8px';
} else if (position === 'right') {
// Layout: [logo at x=0] [innerPadding] [text] [outerPadding]
// Blue box: shows ONLY the text width - FULL HEIGHT
fontSizeIndicator.style.left = `${scaledOriginalWidth + innerPadding}px`;
fontSizeIndicator.style.top = '0px';
fontSizeIndicator.style.bottom = '0px';
fontSizeIndicator.style.width = `${textWidth}px`;
// Font label ABOVE the box, anchored to RIGHT edge of text box
fontSizeLabel.style.right = '0px';
fontSizeLabel.style.bottom = '100%';
fontSizeLabel.style.marginBottom = '8px';
// Green box: shows ONLY innerPadding - FULL HEIGHT
paddingIndicator.style.left = `${scaledOriginalWidth}px`;
paddingIndicator.style.top = '0px';
paddingIndicator.style.bottom = '0px';
paddingIndicator.style.width = `${innerPadding}px`;
// Padding label BELOW the box, anchored to RIGHT edge of padding box
paddingLabel.style.right = '0px';
paddingLabel.style.top = '100%';
paddingLabel.style.marginTop = '8px';
} else {
// position === 'left'
// Layout: [outerPadding] [text] [innerPadding] [logo]
// Blue box: shows ONLY the text width - FULL HEIGHT
fontSizeIndicator.style.left = `${outerPadding}px`;
fontSizeIndicator.style.top = '0px';
fontSizeIndicator.style.bottom = '0px';
fontSizeIndicator.style.width = `${textWidth}px`;
// Font label ABOVE the box, anchored to LEFT edge of text box
fontSizeLabel.style.left = '0px';
fontSizeLabel.style.bottom = '100%';
fontSizeLabel.style.marginBottom = '8px';
// Green box: shows ONLY innerPadding - FULL HEIGHT
paddingIndicator.style.left = `${outerPadding + textWidth}px`;
paddingIndicator.style.top = '0px';
paddingIndicator.style.bottom = '0px';
paddingIndicator.style.width = `${innerPadding}px`;
// Padding label BELOW the box, anchored to LEFT edge of padding box
paddingLabel.style.left = '0px';
paddingLabel.style.top = '100%';
paddingLabel.style.marginTop = '8px';
}
};
fontSizeLabel.textContent = `Font: ${fontSize === 'auto' ? `~${fontSizeNum} (auto)` : fontSize}px`;
paddingLabel.textContent = `Padding: ${padding === 'auto' ? `~${paddingNum} (auto)` : padding}px`;
// Backend uses inner_padding (full padding) and outer_padding (padding/4)
const innerPadding = paddingNum * scale;
const outerPadding = (paddingNum / 4) * scale;
// For horizontal text (left/right positions), we need to estimate actual text width
// Use canvas to measure text width more accurately
const text = document.getElementById('text').value;
let textWidth, textHeight;
if (position === 'left' || position === 'right') {
// For left/right: use actual text width and actual bounding box height
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = `${fontSizeNum}px Arial`;
const metrics = ctx.measureText(text);
textWidth = metrics.width * scale;
// Use actual text bounding box height (tighter than font size)
textHeight = fontSizeNum * 0.75 * scale;
} else {
// For above/below: text height is tighter bounding box
textHeight = fontSizeNum * 0.75 * scale;
textWidth = fontSizeNum * scale; // Not used in above/below
}
// Calculate text_area dimensions (matches backend exactly)
let textAreaHeight, textAreaWidth;
if (position === 'below' || position === 'above') {
textAreaHeight = textHeight + outerPadding + innerPadding;
} else {
textAreaWidth = textWidth + outerPadding + innerPadding;
}
// Calculate where the original logo is positioned in the final image
const scaledOriginalWidth = originalWidth * scale;
const scaledOriginalHeight = originalHeight * scale;
// Reset all styles completely
fontSizeIndicator.style.cssText = 'position: absolute; border: 2px dashed #667eea; background: rgba(102, 126, 234, 0.1); display: block;';
paddingIndicator.style.cssText = 'position: absolute; border: 2px dashed #4caf50; background: rgba(76, 175, 80, 0.1); display: block;';
fontSizeLabel.style.cssText = 'position: absolute; background: #667eea; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;';
paddingLabel.style.cssText = 'position: absolute; background: #4caf50; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;';
if (position === 'below') {
// Layout: [logo at y=0] [innerPadding] [text] [outerPadding]
// Blue box: shows ONLY the text bounding box - FULL WIDTH
fontSizeIndicator.style.left = '0px';
fontSizeIndicator.style.right = '0px';
fontSizeIndicator.style.top = `${scaledOriginalHeight + innerPadding}px`;
fontSizeIndicator.style.height = `${textHeight}px`;
// Font label on the LEFT side, anchored to BOTTOM edge of text box
fontSizeLabel.style.right = '100%';
fontSizeLabel.style.bottom = '0px';
fontSizeLabel.style.marginRight = '8px';
// Green box: shows ONLY innerPadding - FULL WIDTH
paddingIndicator.style.left = '0px';
paddingIndicator.style.right = '0px';
paddingIndicator.style.top = `${scaledOriginalHeight}px`;
paddingIndicator.style.height = `${innerPadding}px`;
// Padding label on the RIGHT side, anchored to BOTTOM edge of padding box
paddingLabel.style.left = '100%';
paddingLabel.style.bottom = '0px';
paddingLabel.style.marginLeft = '8px';
} else if (position === 'above') {
// Layout: [outerPadding] [text] [innerPadding] [logo]
// Blue box: shows ONLY the text bounding box - FULL WIDTH
fontSizeIndicator.style.left = '0px';
fontSizeIndicator.style.right = '0px';
fontSizeIndicator.style.top = `${outerPadding}px`;
fontSizeIndicator.style.height = `${textHeight}px`;
// Font label on the LEFT side, anchored to TOP edge of text box
fontSizeLabel.style.right = '100%';
fontSizeLabel.style.top = '0px';
fontSizeLabel.style.marginRight = '8px';
// Green box: shows ONLY innerPadding - FULL WIDTH
paddingIndicator.style.left = '0px';
paddingIndicator.style.right = '0px';
paddingIndicator.style.top = `${outerPadding + textHeight}px`;
paddingIndicator.style.height = `${innerPadding}px`;
// Padding label on the RIGHT side, anchored to TOP edge of padding box
paddingLabel.style.left = '100%';
paddingLabel.style.top = '0px';
paddingLabel.style.marginLeft = '8px';
} else if (position === 'right') {
// Layout: [logo at x=0] [innerPadding] [text] [outerPadding]
// Blue box: shows ONLY the text width - FULL HEIGHT
fontSizeIndicator.style.left = `${scaledOriginalWidth + innerPadding}px`;
fontSizeIndicator.style.top = '0px';
fontSizeIndicator.style.bottom = '0px';
fontSizeIndicator.style.width = `${textWidth}px`;
// Font label ABOVE the box, anchored to RIGHT edge of text box
fontSizeLabel.style.right = '0px';
fontSizeLabel.style.bottom = '100%';
fontSizeLabel.style.marginBottom = '8px';
// Green box: shows ONLY innerPadding - FULL HEIGHT
paddingIndicator.style.left = `${scaledOriginalWidth}px`;
paddingIndicator.style.top = '0px';
paddingIndicator.style.bottom = '0px';
paddingIndicator.style.width = `${innerPadding}px`;
// Padding label BELOW the box, anchored to RIGHT edge of padding box
paddingLabel.style.right = '0px';
paddingLabel.style.top = '100%';
paddingLabel.style.marginTop = '8px';
} else {
// position === 'left'
// Layout: [outerPadding] [text] [innerPadding] [logo]
// Blue box: shows ONLY the text width - FULL HEIGHT
fontSizeIndicator.style.left = `${outerPadding}px`;
fontSizeIndicator.style.top = '0px';
fontSizeIndicator.style.bottom = '0px';
fontSizeIndicator.style.width = `${textWidth}px`;
// Font label ABOVE the box, anchored to LEFT edge of text box
fontSizeLabel.style.left = '0px';
fontSizeLabel.style.bottom = '100%';
fontSizeLabel.style.marginBottom = '8px';
// Green box: shows ONLY innerPadding - FULL HEIGHT
paddingIndicator.style.left = `${outerPadding + textWidth}px`;
paddingIndicator.style.top = '0px';
paddingIndicator.style.bottom = '0px';
paddingIndicator.style.width = `${innerPadding}px`;
// Padding label BELOW the box, anchored to LEFT edge of padding box
paddingLabel.style.left = '0px';
paddingLabel.style.top = '100%';
paddingLabel.style.marginTop = '8px';
}
updateVisualization();
}
showDimensions.addEventListener('change', updateDimensionOverlay);
@@ -1893,6 +1855,92 @@
imageFile.addEventListener('change', () => {
setTimeout(triggerLivePreview, 100);
});
// API URL generation and updating
const apiUrlInput = document.getElementById('apiUrl');
function updateApiUrl() {
const text = document.getElementById('text').value;
// Don't show API URL if no text or no image source
if (!text.trim()) {
apiUrlInput.value = '';
return;
}
let imageUrl = '';
// Get the image URL based on active input mode
if (activeInputMode === 'browse') {
imageUrl = selectedLogoUrl.value.trim();
} else if (activeInputMode === 'url') {
imageUrl = document.getElementById('imageUrl').value.trim();
} else {
// File upload mode - can't generate API URL
apiUrlInput.value = 'API URL only available for URL/Browse modes (not file upload)';
return;
}
if (!imageUrl) {
apiUrlInput.value = '';
return;
}
// Build API URL
const position = document.querySelector('input[name="position"]:checked').value;
const fontSizeAutoChecked = document.getElementById('fontSizeAuto').checked;
const fontSize = fontSizeAutoChecked ? 'auto' : document.getElementById('fontSize').value;
const paddingAutoChecked = document.getElementById('paddingAuto').checked;
const padding = paddingAutoChecked ? 'auto' : document.getElementById('padding').value;
const textColor = textColorInput.value;
const bgColor = bgColorInput.value;
const fontPath = fontSelect.value;
const params = new URLSearchParams({
url: imageUrl,
text: text,
position: position,
font_size: fontSize,
padding: padding,
text_color: textColor,
bg_color: bgColor
});
// Only include font_path if it's not 'auto' (since auto is the default)
if (fontPath && fontPath !== 'auto') {
params.append('font_path', fontPath);
}
// Build full URL (use current origin)
const baseUrl = window.location.origin;
apiUrlInput.value = `${baseUrl}/api/image?${params.toString()}`;
}
// Update API URL whenever relevant inputs change
document.getElementById('text').addEventListener('input', updateApiUrl);
document.querySelectorAll('input[name="position"]').forEach(radio => {
radio.addEventListener('change', updateApiUrl);
});
fontSizeInput.addEventListener('input', updateApiUrl);
fontSizeSlider.addEventListener('input', updateApiUrl);
fontSizeAuto.addEventListener('change', updateApiUrl);
paddingInput.addEventListener('input', updateApiUrl);
paddingSlider.addEventListener('input', updateApiUrl);
paddingAuto.addEventListener('change', updateApiUrl);
textColorInput.addEventListener('input', updateApiUrl);
textColorPicker.addEventListener('input', updateApiUrl);
bgColorInput.addEventListener('input', updateApiUrl);
bgColorPicker.addEventListener('input', updateApiUrl);
fontSelect.addEventListener('change', updateApiUrl);
imageUrl.addEventListener('input', updateApiUrl);
selectedLogoUrl.addEventListener('change', updateApiUrl);
// Update when tab changes
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
setTimeout(updateApiUrl, 100);
});
});
</script>
</body>
</html>