Files
logo-txt/templates/index.html
Seth Van Niekerk d26f619901 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
2026-01-17 13:57:47 -05:00

1947 lines
77 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>logo-txt</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0f0f0f;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1800px;
margin: 0 auto;
background: #1a1a1a;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
overflow: hidden;
border: 1px solid #333;
}
.main-layout {
display: grid;
grid-template-columns: 1fr;
gap: 0;
}
@media (min-width: 1200px) {
body {
padding: 0 !important;
}
.container {
border-radius: 0 !important;
border: none !important;
}
.main-layout {
grid-template-columns: 500px 1fr;
}
.form-container {
border-right: 1px solid #333;
max-height: 100vh;
overflow-y: auto;
}
.preview-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 40px;
background: #0f0f0f;
}
.preview-section {
max-width: 100%;
}
.preview-section br {
display: none;
}
.logo-grid {
grid-template-columns: repeat(4, 1fr) !important;
grid-template-rows: repeat(2, 110px) !important;
grid-auto-rows: 110px !important;
max-height: 215px !important;
gap: 8px !important;
padding: 8px !important;
overflow-y: auto !important;
}
.logo-item {
height: 110px !important;
}
.logo-item-name {
font-size: 9px !important;
height: 22px !important;
}
.logo-item img {
height: calc(100% - 22px) !important;
padding: 6px !important;
}
/* Compact form controls for two-column layout */
.content {
padding: 8px !important;
}
.form-group {
margin-bottom: 6px !important;
}
label {
margin-bottom: 2px !important;
font-size: 0.75em !important;
}
input[type="text"],
input[type="number"],
select {
padding: 5px 7px !important;
font-size: 12px !important;
}
.file-input-label {
padding: 6px !important;
font-size: 12px !important;
}
.position-option label {
padding: 6px !important;
font-size: 12px !important;
}
input[type="color"] {
width: 40px !important;
height: 32px !important;
}
.tab-btn {
padding: 5px 8px !important;
font-size: 11px !important;
}
.preview-section {
margin-top: 10px;
padding-top: 10px;
border-top: none !important;
}
.preview-section h2 {
margin-bottom: 10px;
font-size: 1.1em;
}
.search-controls {
gap: 6px;
margin-bottom: 6px;
}
.two-column {
gap: 6px;
}
small {
font-size: 0.7em;
}
}
.content {
padding: 40px;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #e0e0e0;
font-size: 0.95em;
}
input[type="text"],
input[type="number"],
select {
width: 100%;
padding: 12px 15px;
border: 2px solid #333;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
background: #2a2a2a;
color: #e0e0e0;
}
input[type="text"]:focus,
input[type="number"]:focus,
select:focus {
outline: none;
border-color: #667eea;
}
.file-input-wrapper {
position: relative;
overflow: hidden;
display: inline-block;
width: 100%;
}
.file-input-wrapper input[type="file"] {
position: absolute;
left: -9999px;
}
.file-input-label {
display: block;
padding: 15px;
background: #2a2a2a;
border: 2px dashed #667eea;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
color: #e0e0e0;
}
.file-input-label:hover {
background: #333;
border-color: #764ba2;
}
.file-input-label.has-file {
background: #1e3a1e;
border-color: #4caf50;
color: #b3ffb3;
}
.color-input-group {
display: flex;
gap: 10px;
align-items: center;
}
.color-input-group input[type="text"] {
flex: 1;
}
input[type="color"] {
width: 60px;
height: 45px;
border: 2px solid #333;
border-radius: 8px;
cursor: pointer;
background: #2a2a2a;
}
.position-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.position-option {
position: relative;
}
.position-option input[type="radio"] {
position: absolute;
opacity: 0;
}
.position-option label {
display: block;
padding: 15px;
background: #2a2a2a;
border: 2px solid #333;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
margin: 0;
color: #e0e0e0;
}
.position-option input[type="radio"]:checked + label {
background: #667eea;
color: white;
border-color: #667eea;
}
.position-option label:hover {
border-color: #667eea;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 30px;
border: none;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
width: 100%;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
}
.btn:active {
transform: translateY(0);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.preview-section {
margin-top: 30px;
padding-top: 30px;
border-top: 2px solid #333;
text-align: center;
}
.preview-section h2 {
margin-bottom: 20px;
color: #e0e0e0;
}
.preview-image {
max-width: 100%;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
background-image:
linear-gradient(45deg, #808080 25%, transparent 25%),
linear-gradient(-45deg, #808080 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #808080 75%),
linear-gradient(-45deg, transparent 75%, #808080 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
background-color: #999999;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s, opacity 0.3s;
}
.preview-image:hover {
transform: scale(1.02);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
}
.preview-image.loading {
opacity: 0.5;
pointer-events: none;
}
.preview-loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(15, 15, 15, 0.7);
border-radius: 8px;
z-index: 10;
}
.preview-loading-overlay.active {
display: flex;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(102, 126, 234, 0.2);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.preview-download-hint {
display: inline-block;
margin-top: 10px;
padding: 8px 16px;
background: rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 6px;
color: #667eea;
font-size: 13px;
font-weight: 500;
}
.preview-download-hint::before {
content: '⬇️ ';
}
.loading {
text-align: center;
padding: 20px;
color: #667eea;
font-weight: 600;
}
.error {
background: #ffebee;
color: #c62828;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #c62828;
}
.success {
background: #e8f5e9;
color: #2e7d32;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #2e7d32;
}
.download-btn {
display: inline-block;
margin-top: 15px;
padding: 12px 25px;
background: #4caf50;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s;
}
.download-btn:hover {
background: #45a049;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
}
.advanced-options {
margin-top: 20px;
padding: 20px;
background: #2a2a2a;
border-radius: 8px;
border: 1px solid #333;
}
.advanced-options h3 {
margin-bottom: 15px;
color: #e0e0e0;
font-size: 1.1em;
}
.two-column {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.input-tabs {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.tab-btn {
flex: 1;
padding: 10px 20px;
background: #2a2a2a;
border: 2px solid #333;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
color: #999;
transition: all 0.3s;
}
.tab-btn:hover {
border-color: #667eea;
}
.tab-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Custom Font Selector Styles */
.custom-select-wrapper {
position: relative;
width: 100%;
}
.custom-select {
width: 100%;
padding: 12px 40px 12px 15px;
border: 2px solid #333;
border-radius: 8px;
font-size: 16px;
background: #2a2a2a;
color: #e0e0e0;
cursor: pointer;
transition: border-color 0.3s;
user-select: none;
position: relative;
}
.custom-select:hover {
border-color: #667eea;
}
.custom-select.open {
border-color: #667eea;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.custom-select::after {
content: '▼';
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
color: #666;
pointer-events: none;
}
.custom-select.open::after {
transform: translateY(-50%) rotate(180deg);
}
.custom-options {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 300px;
overflow-y: auto;
background: #2a2a2a;
border: 2px solid #667eea;
border-top: none;
border-radius: 0 0 8px 8px;
z-index: 1000;
display: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
.custom-options.open {
display: block;
}
.custom-option {
padding: 12px 15px;
cursor: pointer;
transition: background 0.2s;
font-size: 16px;
color: #e0e0e0;
}
.custom-option:hover {
background: #333;
}
.custom-option.selected {
background: #667eea;
color: white;
font-weight: 600;
}
.custom-option:first-child {
font-family: inherit;
}
/* Logo Browser Styles */
.search-controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.search-controls input {
flex: 2;
}
.search-controls select {
flex: 1;
}
.logo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
max-height: 400px;
overflow-y: auto;
padding: 10px;
background: #2a2a2a;
border-radius: 8px;
margin-bottom: 10px;
border: 1px solid #333;
}
.logo-item {
aspect-ratio: 3/4;
background:
linear-gradient(45deg, #808080 25%, transparent 25%),
linear-gradient(-45deg, #808080 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #808080 75%),
linear-gradient(-45deg, transparent 75%, #808080 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
background-color: #999999;
border: 2px solid #444;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.logo-item:hover {
border-color: #667eea;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
}
.logo-item.selected {
border-color: #4caf50;
background-color: #558855;
}
.logo-item img {
width: 100%;
height: calc(100% - 28px);
object-fit: contain;
padding: 8px;
}
.logo-item-name {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(26, 26, 26, 0.95);
border-top: 1px solid #444;
font-size: 10px;
color: #e0e0e0;
font-weight: 500;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 6px;
}
.loading-spinner {
text-align: center;
padding: 20px;
color: #667eea;
}
@media (max-width: 600px) {
.two-column, .position-grid {
grid-template-columns: 1fr;
}
.header h1 {
font-size: 1.8em;
}
.content {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="main-layout">
<div class="form-container">
<div class="content">
<div id="message"></div>
<form id="logoForm">
<div class="form-group">
<label>Logo Image</label>
<div class="input-tabs">
<button type="button" class="tab-btn active" data-tab="file">📁 Upload File</button>
<button type="button" class="tab-btn" data-tab="url">🔗 Image URL</button>
<button type="button" class="tab-btn" data-tab="browse">📺 Browse Logos</button>
</div>
<div id="fileTab" class="tab-content active">
<div class="file-input-wrapper">
<input type="file" id="imageFile" accept="image/*">
<label for="imageFile" class="file-input-label" id="fileLabel">
<span>📁 Choose an image file (PNG, JPG, GIF, WEBP)</span>
</label>
</div>
</div>
<div id="urlTab" class="tab-content">
<input type="text" id="imageUrl" placeholder="https://example.com/logo.png">
</div>
<div id="browseTab" class="tab-content">
<div class="search-controls">
<input type="text" id="logoSearch" placeholder="🔍 Search by filename...">
<select id="countryFilter">
<option value="">All Countries</option>
</select>
</div>
<div id="logoGrid" class="logo-grid">
<div class="loading-spinner">Loading logos...</div>
</div>
<input type="hidden" id="selectedLogoUrl">
</div>
</div>
<div class="form-group">
<label for="text">Text to Add</label>
<input type="text" id="text" placeholder="Enter your text here..." required>
</div>
<div class="form-group">
<label>Text Position</label>
<div class="position-grid">
<div class="position-option">
<input type="radio" id="above" name="position" value="above">
<label for="above">⬆️ Above</label>
</div>
<div class="position-option">
<input type="radio" id="below" name="position" value="below" checked>
<label for="below">⬇️ Below</label>
</div>
<div class="position-option">
<input type="radio" id="left" name="position" value="left">
<label for="left">⬅️ Left</label>
</div>
<div class="position-option">
<input type="radio" id="right" name="position" value="right">
<label for="right">➡️ Right</label>
</div>
</div>
</div>
<div class="advanced-options">
<h3>⚙️ Advanced Options</h3>
<div class="form-group">
<label for="fontSelect">Font</label>
<div class="custom-select-wrapper">
<div class="custom-select" id="customFontSelect">
<span id="selectedFontText">Auto (Best Match)</span>
</div>
<div class="custom-options" id="customFontOptions">
<!-- Options will be populated by JavaScript -->
</div>
</div>
<input type="hidden" id="fontSelect" value="auto">
<small id="usedFontDisplay" style="color: #667eea; font-size: 0.85em; display: none; margin-top: 4px;"></small>
</div>
<div class="two-column">
<div class="form-group">
<label for="fontSize">Font Size (pixels)</label>
<div style="display: flex; gap: 8px; align-items: center;">
<input type="number" id="fontSize" value="80" min="10" max="500" step="5" style="flex: 1;">
<label style="display: flex; align-items: center; gap: 6px; margin: 0; cursor: pointer; white-space: nowrap; font-weight: normal; color: #999;">
<input type="checkbox" id="fontSizeAuto" checked style="cursor: pointer;">
Auto
</label>
</div>
<input type="range" id="fontSizeSlider" min="10" max="500" step="5" value="80" style="width: 100%; margin-top: 8px; cursor: pointer;" disabled>
<small id="usedFontSizeDisplay" style="color: #667eea; font-size: 0.85em; display: none; margin-top: 4px;"></small>
</div>
<div class="form-group">
<label for="padding">Padding (pixels)</label>
<div style="display: flex; gap: 8px; align-items: center;">
<input type="number" id="padding" value="20" min="0" max="200" step="5" style="flex: 1;">
<label style="display: flex; align-items: center; gap: 6px; margin: 0; cursor: pointer; white-space: nowrap; font-weight: normal; color: #999;">
<input type="checkbox" id="paddingAuto" checked style="cursor: pointer;">
Auto
</label>
</div>
<input type="range" id="paddingSlider" min="0" max="200" step="5" value="20" style="width: 100%; margin-top: 8px; cursor: pointer;" disabled>
<small id="usedPaddingDisplay" style="color: #667eea; font-size: 0.85em; display: none; margin-top: 4px;"></small>
</div>
</div>
<div class="form-group">
<label for="textColor">Text Color</label>
<div class="color-input-group">
<input type="text" id="textColor" value="white" placeholder="white or #ffffff">
<input type="color" id="textColorPicker" value="#ffffff">
</div>
</div>
<div class="form-group">
<label for="bgColor">Background Color</label>
<div class="color-input-group">
<input type="text" id="bgColor" value="transparent" placeholder="transparent or #1a1a1a">
<input type="color" id="bgColorPicker" value="#1a1a1a">
</div>
<small style="color: #666; font-size: 0.85em;">Use "transparent" to preserve logo transparency</small>
</div>
</div>
<button type="submit" class="btn" id="submitBtn" style="display: none;">
✨ Generate Logo
</button>
</form>
</div>
</div>
<div class="preview-container">
<div id="preview" class="preview-section" style="display: none; width: 100%;">
<h2>Preview</h2>
<div style="padding: 30px;">
<div style="position: relative; display: inline-block;">
<img id="previewImage" class="preview-image" alt="Processed logo">
<div id="previewLoadingOverlay" class="preview-loading-overlay">
<div class="spinner"></div>
</div>
<div id="dimensionOverlay" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; display: none;">
<!-- Font size indicator -->
<div id="fontSizeIndicator" style="position: absolute; border: 2px dashed #667eea; background: rgba(102, 126, 234, 0.1); display: none;">
<div id="fontSizeLabel" style="position: absolute; background: #667eea; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;"></div>
</div>
<!-- Padding indicator -->
<div id="paddingIndicator" style="position: absolute; border: 2px dashed #4caf50; background: rgba(76, 175, 80, 0.1); display: none;">
<div id="paddingLabel" style="position: absolute; background: #4caf50; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap;"></div>
</div>
</div>
</div>
</div>
<br>
<label style="display: flex; align-items: center; gap: 8px; margin: 10px auto; cursor: pointer; color: #e0e0e0; font-weight: normal; font-size: 14px; justify-content: center; max-width: 250px;">
<input type="checkbox" id="showDimensions" checked style="cursor: pointer;">
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>
<script>
const form = document.getElementById('logoForm');
const imageFile = document.getElementById('imageFile');
const imageUrl = document.getElementById('imageUrl');
const fileLabel = document.getElementById('fileLabel');
const submitBtn = document.getElementById('submitBtn');
const messageDiv = document.getElementById('message');
const preview = document.getElementById('preview');
const previewImage = document.getElementById('previewImage');
const previewLoadingOverlay = document.getElementById('previewLoadingOverlay');
const tabBtns = document.querySelectorAll('.tab-btn');
const fileTab = document.getElementById('fileTab');
const urlTab = document.getElementById('urlTab');
const browseTab = document.getElementById('browseTab');
const logoSearch = document.getElementById('logoSearch');
const countryFilter = document.getElementById('countryFilter');
const logoGrid = document.getElementById('logoGrid');
const selectedLogoUrl = document.getElementById('selectedLogoUrl');
const fontSelect = document.getElementById('fontSelect');
const customFontSelect = document.getElementById('customFontSelect');
const customFontOptions = document.getElementById('customFontOptions');
const selectedFontText = document.getElementById('selectedFontText');
const showDimensions = document.getElementById('showDimensions');
const dimensionOverlay = document.getElementById('dimensionOverlay');
const fontSizeIndicator = document.getElementById('fontSizeIndicator');
const paddingIndicator = document.getElementById('paddingIndicator');
const fontSizeLabel = document.getElementById('fontSizeLabel');
const paddingLabel = document.getElementById('paddingLabel');
let activeInputMode = 'file';
let availableFonts = [];
let allLogos = [];
let allCountries = [];
let displayedCount = 50;
let currentFilteredLogos = [];
let isLoadingMore = false;
// Custom select functionality
customFontSelect.addEventListener('click', (e) => {
e.stopPropagation();
customFontSelect.classList.toggle('open');
customFontOptions.classList.toggle('open');
});
// Close dropdown when clicking outside
document.addEventListener('click', () => {
customFontSelect.classList.remove('open');
customFontOptions.classList.remove('open');
});
function selectFont(value, text, fontFamily) {
fontSelect.value = value;
selectedFontText.textContent = text;
if (fontFamily) {
selectedFontText.style.fontFamily = fontFamily;
} else {
selectedFontText.style.fontFamily = 'inherit';
}
// Update selected state in options
document.querySelectorAll('.custom-option').forEach(opt => {
opt.classList.remove('selected');
});
event.target.classList.add('selected');
customFontSelect.classList.remove('open');
customFontOptions.classList.remove('open');
// Trigger live preview when font changes
triggerLivePreview();
// Update API URL when font changes
updateApiUrl();
}
// Load available fonts
async function loadFonts() {
try {
const response = await fetch('/api/fonts');
const data = await response.json();
availableFonts = data.fonts;
// Clear existing options
customFontOptions.innerHTML = '';
// Add "Auto" option
const autoOption = document.createElement('div');
autoOption.className = 'custom-option selected';
autoOption.textContent = 'Auto (Best Match)';
autoOption.addEventListener('click', () => {
selectFont('auto', 'Auto (Best Match)', null);
});
customFontOptions.appendChild(autoOption);
// Populate font options with font previews
availableFonts.forEach(font => {
const option = document.createElement('div');
option.className = 'custom-option';
option.textContent = font.name;
option.dataset.value = font.path;
// Apply the font's style to the option
const fontFamily = getFontFamilyForPreview(font.name);
option.style.fontFamily = fontFamily;
option.addEventListener('click', () => {
selectFont(font.path, font.name, fontFamily);
});
customFontOptions.appendChild(option);
});
} catch (error) {
console.error('Failed to load fonts:', error);
}
}
// Map font names to CSS font families for preview
function getFontFamilyForPreview(fontName) {
const lowerName = fontName.toLowerCase();
// Google Fonts
if (lowerName.includes('roboto')) return 'Roboto, sans-serif';
if (lowerName.includes('open sans')) return 'Open Sans, sans-serif';
if (lowerName.includes('montserrat')) return 'Montserrat, sans-serif';
if (lowerName.includes('oswald')) return 'Oswald, sans-serif';
if (lowerName.includes('raleway')) return 'Raleway, sans-serif';
if (lowerName.includes('lato')) return 'Lato, sans-serif';
if (lowerName.includes('poppins')) return 'Poppins, sans-serif';
if (lowerName.includes('anton')) return 'Anton, sans-serif';
if (lowerName.includes('bebas neue')) return 'Bebas Neue, sans-serif';
if (lowerName.includes('pt sans')) return 'PT Sans, sans-serif';
if (lowerName.includes('nunito')) return 'Nunito, sans-serif';
// DejaVu fonts
if (lowerName.includes('dejavu sans mono')) return 'DejaVu Sans Mono, monospace';
if (lowerName.includes('dejavu sans')) return 'DejaVu Sans, sans-serif';
if (lowerName.includes('dejavu serif')) return 'DejaVu Serif, serif';
// Liberation fonts
if (lowerName.includes('liberation sans')) return 'Liberation Sans, sans-serif';
if (lowerName.includes('liberation serif')) return 'Liberation Serif, serif';
if (lowerName.includes('liberation mono')) return 'Liberation Mono, monospace';
// Noto fonts
if (lowerName.includes('noto sans')) return 'Noto Sans, sans-serif';
if (lowerName.includes('noto serif')) return 'Noto Serif, serif';
if (lowerName.includes('noto mono')) return 'Noto Mono, monospace';
// FreeFonts
if (lowerName.includes('freesans')) return 'FreeSans, sans-serif';
if (lowerName.includes('freeserif')) return 'FreeSerif, serif';
if (lowerName.includes('freemono')) return 'FreeMono, monospace';
// Default to font name
return fontName + ', sans-serif';
}
// Load fonts on page load
loadFonts();
// Load TV logos
async function loadTVLogos(search = '', country = '') {
try {
logoGrid.innerHTML = '<div class="loading-spinner">Loading logos...</div>';
const params = new URLSearchParams();
if (search) params.append('search', search);
if (country) params.append('country', country);
const response = await fetch(`/api/tv-logos?${params}`);
const data = await response.json();
if (data.error) {
logoGrid.innerHTML = `<div style="padding: 20px; text-align: center; color: #c62828;">${data.error}</div>`;
return;
}
allLogos = data.logos;
// Populate country filter if not already done
if (allCountries.length === 0 && data.countries) {
allCountries = data.countries;
data.countries.forEach(country => {
const option = document.createElement('option');
option.value = country;
option.textContent = country;
countryFilter.appendChild(option);
});
}
// Reset display count and render first batch
displayedCount = 50;
currentFilteredLogos = allLogos;
renderLogos(allLogos);
} catch (error) {
console.error('Failed to load logos:', error);
logoGrid.innerHTML = '<div style="padding: 20px; text-align: center; color: #c62828;">Failed to load logos. Please try again.</div>';
}
}
function renderLogos(logos) {
currentFilteredLogos = logos;
displayedCount = 50;
if (logos.length === 0) {
logoGrid.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">No logos found.</div>';
return;
}
logoGrid.innerHTML = '';
// Only render first batch
const logosToShow = logos.slice(0, displayedCount);
logosToShow.forEach(logo => {
const logoItem = document.createElement('div');
logoItem.className = 'logo-item';
logoItem.innerHTML = `
<img src="${logo.thumbnail}" alt="${logo.name}" loading="lazy">
<div class="logo-item-name" title="${logo.name} (${logo.country})">${logo.name}</div>
`;
logoItem.addEventListener('click', () => {
// Remove selected class from all items
document.querySelectorAll('.logo-item').forEach(item => {
item.classList.remove('selected');
});
// Add selected class to clicked item
logoItem.classList.add('selected');
// Store the selected logo URL
selectedLogoUrl.value = logo.url;
// Manually trigger live preview since programmatic changes don't fire 'change' event
triggerLivePreview();
});
logoGrid.appendChild(logoItem);
});
// Add load more indicator if there are more logos
if (logosToShow.length < logos.length) {
const loadMore = document.createElement('div');
loadMore.id = 'loadMoreIndicator';
loadMore.style.cssText = 'grid-column: 1 / -1; padding: 20px; text-align: center; color: #999; font-size: 14px;';
loadMore.textContent = `Showing ${logosToShow.length} of ${logos.length} logos - scroll for more`;
logoGrid.appendChild(loadMore);
}
}
function loadMoreLogos() {
if (isLoadingMore || displayedCount >= currentFilteredLogos.length) {
return;
}
isLoadingMore = true;
// Get next batch
const nextBatch = currentFilteredLogos.slice(displayedCount, displayedCount + 50);
displayedCount += 50;
// Remove load more indicator
const loadMoreIndicator = document.getElementById('loadMoreIndicator');
if (loadMoreIndicator) {
loadMoreIndicator.remove();
}
// Append new logos
nextBatch.forEach(logo => {
const logoItem = document.createElement('div');
logoItem.className = 'logo-item';
logoItem.innerHTML = `
<img src="${logo.thumbnail}" alt="${logo.name}" loading="lazy">
<div class="logo-item-name" title="${logo.name} (${logo.country})">${logo.name}</div>
`;
logoItem.addEventListener('click', () => {
document.querySelectorAll('.logo-item').forEach(item => {
item.classList.remove('selected');
});
logoItem.classList.add('selected');
selectedLogoUrl.value = logo.url;
// Manually trigger live preview since programmatic changes don't fire 'change' event
triggerLivePreview();
});
logoGrid.appendChild(logoItem);
});
// Add load more indicator if there are still more
if (displayedCount < currentFilteredLogos.length) {
const loadMore = document.createElement('div');
loadMore.id = 'loadMoreIndicator';
loadMore.style.cssText = 'grid-column: 1 / -1; padding: 20px; text-align: center; color: #999; font-size: 14px;';
loadMore.textContent = `Showing ${displayedCount} of ${currentFilteredLogos.length} logos - scroll for more`;
logoGrid.appendChild(loadMore);
}
isLoadingMore = false;
}
// Debounce function for search
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Search and filter handlers
const debouncedSearch = debounce(() => {
const search = logoSearch.value;
const country = countryFilter.value;
// Filter logos client-side for instant results (searches ALL logos)
let filtered = allLogos;
// Filter by filename only
if (search) {
const searchLower = search.toLowerCase();
filtered = filtered.filter(logo =>
logo.name.toLowerCase().includes(searchLower)
);
}
// Filter by country
if (country) {
filtered = filtered.filter(logo => logo.country === country);
}
// Reset display count and render (shows first 50)
renderLogos(filtered);
}, 300);
logoSearch.addEventListener('input', debouncedSearch);
countryFilter.addEventListener('change', debouncedSearch);
// Tab switching
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.dataset.tab;
activeInputMode = tab;
// Update button states
tabBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Update tab content
fileTab.classList.remove('active');
urlTab.classList.remove('active');
browseTab.classList.remove('active');
if (tab === 'file') {
fileTab.classList.add('active');
} else if (tab === 'url') {
urlTab.classList.add('active');
} else if (tab === 'browse') {
browseTab.classList.add('active');
// Load logos when browse tab is opened for the first time
if (allLogos.length === 0) {
loadTVLogos();
}
}
});
});
// Infinite scroll for Browse Logos tab
logoGrid.addEventListener('scroll', () => {
const scrollTop = logoGrid.scrollTop;
const scrollHeight = logoGrid.scrollHeight;
const clientHeight = logoGrid.clientHeight;
// Load more when user scrolls to 80% of the content
if (scrollTop + clientHeight >= scrollHeight * 0.8) {
loadMoreLogos();
}
});
// Color picker sync
const textColorInput = document.getElementById('textColor');
const textColorPicker = document.getElementById('textColorPicker');
const bgColorInput = document.getElementById('bgColor');
const bgColorPicker = document.getElementById('bgColorPicker');
textColorPicker.addEventListener('input', (e) => {
textColorInput.value = e.target.value;
});
textColorInput.addEventListener('input', (e) => {
if (e.target.value.startsWith('#')) {
textColorPicker.value = e.target.value;
}
});
bgColorPicker.addEventListener('input', (e) => {
bgColorInput.value = e.target.value;
});
bgColorInput.addEventListener('input', (e) => {
if (e.target.value.startsWith('#') && e.target.value.length === 7) {
bgColorPicker.value = e.target.value;
}
});
// Font size and padding auto toggle
const fontSizeInput = document.getElementById('fontSize');
const fontSizeAuto = document.getElementById('fontSizeAuto');
const fontSizeSlider = document.getElementById('fontSizeSlider');
const paddingInput = document.getElementById('padding');
const paddingAuto = document.getElementById('paddingAuto');
const paddingSlider = document.getElementById('paddingSlider');
// Sync slider with number input
fontSizeSlider.addEventListener('input', (e) => {
fontSizeInput.value = e.target.value;
});
fontSizeInput.addEventListener('input', (e) => {
fontSizeSlider.value = e.target.value;
});
paddingSlider.addEventListener('input', (e) => {
paddingInput.value = e.target.value;
});
paddingInput.addEventListener('input', (e) => {
paddingSlider.value = e.target.value;
});
fontSizeAuto.addEventListener('change', (e) => {
fontSizeInput.disabled = e.target.checked;
fontSizeSlider.disabled = e.target.checked;
if (e.target.checked) {
fontSizeInput.style.opacity = '0.5';
fontSizeSlider.style.opacity = '0.5';
} else {
fontSizeInput.style.opacity = '1';
fontSizeSlider.style.opacity = '1';
document.getElementById('usedFontSizeDisplay').style.display = 'none';
}
});
paddingAuto.addEventListener('change', (e) => {
paddingInput.disabled = e.target.checked;
paddingSlider.disabled = e.target.checked;
if (e.target.checked) {
paddingInput.style.opacity = '0.5';
paddingSlider.style.opacity = '0.5';
} else {
paddingInput.style.opacity = '1';
paddingSlider.style.opacity = '1';
document.getElementById('usedPaddingDisplay').style.display = 'none';
}
});
// Initialize disabled state
fontSizeInput.disabled = fontSizeAuto.checked;
fontSizeSlider.disabled = fontSizeAuto.checked;
paddingInput.disabled = paddingAuto.checked;
paddingSlider.disabled = paddingAuto.checked;
if (fontSizeAuto.checked) {
fontSizeInput.style.opacity = '0.5';
fontSizeSlider.style.opacity = '0.5';
}
if (paddingAuto.checked) {
paddingInput.style.opacity = '0.5';
paddingSlider.style.opacity = '0.5';
}
// Dimension overlay toggle and update
function updateDimensionOverlay() {
if (!showDimensions.checked || !previewImage.dataset.fontSize) {
dimensionOverlay.style.display = 'none';
fontSizeIndicator.style.display = 'none';
paddingIndicator.style.display = 'none';
return;
}
dimensionOverlay.style.display = 'block';
const fontSize = previewImage.dataset.fontSize;
const padding = previewImage.dataset.padding;
const position = document.querySelector('input[name="position"]:checked').value;
const originalWidth = parseInt(previewImage.dataset.originalWidth);
const originalHeight = parseInt(previewImage.dataset.originalHeight);
const imgWidth = previewImage.offsetWidth;
const imgHeight = previewImage.offsetHeight;
const naturalWidth = previewImage.naturalWidth;
const naturalHeight = previewImage.naturalHeight;
const scale = imgWidth / naturalWidth;
// 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 = parseInt(fontSize);
paddingNum = 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';
}
};
updateVisualization();
}
showDimensions.addEventListener('change', updateDimensionOverlay);
previewImage.addEventListener('load', updateDimensionOverlay);
window.addEventListener('resize', updateDimensionOverlay);
// Update visualization when position changes
document.querySelectorAll('input[name="position"]').forEach(radio => {
radio.addEventListener('change', updateDimensionOverlay);
});
// Click preview image to download
previewImage.addEventListener('click', () => {
if (previewImage.dataset.downloadUrl) {
const a = document.createElement('a');
a.href = previewImage.dataset.downloadUrl;
a.download = previewImage.dataset.downloadName || 'logo.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
});
// File input handling
imageFile.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
fileLabel.classList.add('has-file');
fileLabel.innerHTML = `<span>✅ ${file.name}</span>`;
} else {
fileLabel.classList.remove('has-file');
fileLabel.innerHTML = '<span>📁 Choose an image file (PNG, JPG, GIF, WEBP)</span>';
}
});
function showMessage(message, type) {
messageDiv.innerHTML = `<div class="${type}">${message}</div>`;
messageDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function clearMessage() {
messageDiv.innerHTML = '';
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const text = document.getElementById('text').value;
if (!text.trim()) {
return;
}
const position = document.querySelector('input[name="position"]:checked').value;
const fontSizeAuto = document.getElementById('fontSizeAuto').checked;
const fontSize = fontSizeAuto ? 'auto' : document.getElementById('fontSize').value;
const paddingAuto = document.getElementById('paddingAuto').checked;
const padding = paddingAuto ? 'auto' : document.getElementById('padding').value;
const textColor = textColorInput.value;
const bgColor = bgColorInput.value;
const fontPath = fontSelect.value;
submitBtn.disabled = true;
submitBtn.textContent = '⏳ Processing...';
// Show loading state
if (preview.style.display === 'block') {
previewImage.classList.add('loading');
previewLoadingOverlay.classList.add('active');
} else {
preview.style.display = 'block';
previewLoadingOverlay.classList.add('active');
}
try {
let response;
let originalImageWidth = null;
let originalImageHeight = null;
if (activeInputMode === 'browse') {
// Use selected logo from browse
const url = selectedLogoUrl.value.trim();
if (!url) {
submitBtn.disabled = false;
submitBtn.textContent = '✨ Generate Logo';
return;
}
// Get original dimensions from the selected logo
const selectedLogoImg = document.querySelector(`.logo-item.selected img`);
if (selectedLogoImg && selectedLogoImg.complete) {
originalImageWidth = selectedLogoImg.naturalWidth;
originalImageHeight = selectedLogoImg.naturalHeight;
}
const params = new URLSearchParams({
url: url,
text: text,
position: position,
font_size: fontSize,
padding: padding,
text_color: textColor,
bg_color: bgColor,
font_path: fontPath
});
response = await fetch(`/api/image?${params}`);
} else if (activeInputMode === 'url') {
// Use URL-based API
const url = imageUrl.value.trim();
if (!url) {
submitBtn.disabled = false;
submitBtn.textContent = '✨ Generate Logo';
return;
}
// Try to get dimensions from URL image
try {
const tempImg = new Image();
tempImg.crossOrigin = "anonymous";
await new Promise((resolve, reject) => {
tempImg.onload = resolve;
tempImg.onerror = () => resolve(); // Continue even if CORS fails
tempImg.src = url;
setTimeout(() => resolve(), 1000); // Timeout after 1s
});
if (tempImg.complete && tempImg.naturalWidth) {
originalImageWidth = tempImg.naturalWidth;
originalImageHeight = tempImg.naturalHeight;
}
} catch (e) {
// Ignore errors, continue without dimensions
}
const params = new URLSearchParams({
url: url,
text: text,
position: position,
font_size: fontSize,
padding: padding,
text_color: textColor,
bg_color: bgColor,
font_path: fontPath
});
response = await fetch(`/api/image?${params}`);
} else {
// Use file upload API
const file = imageFile.files[0];
if (!file) {
submitBtn.disabled = false;
submitBtn.textContent = '✨ Generate Logo';
return;
}
// Get dimensions from file
try {
const tempImg = new Image();
const reader = new FileReader();
await new Promise((resolve) => {
reader.onload = (e) => {
tempImg.onload = () => {
originalImageWidth = tempImg.naturalWidth;
originalImageHeight = tempImg.naturalHeight;
resolve();
};
tempImg.src = e.target.result;
};
reader.readAsDataURL(file);
});
} catch (e) {
// Continue without dimensions
}
const formData = new FormData();
formData.append('image', file);
formData.append('text', text);
formData.append('position', position);
formData.append('font_size', fontSize);
formData.append('padding', padding);
formData.append('text_color', textColor);
formData.append('bg_color', bgColor);
formData.append('font_path', fontPath);
response = await fetch('/api/process', {
method: 'POST',
body: formData
});
}
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to process image');
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
// Read metadata from response headers
const fontSizeUsed = response.headers.get('X-Font-Size-Used');
const paddingUsed = response.headers.get('X-Padding-Used');
const fontNameUsed = response.headers.get('X-Font-Name-Used');
// Update input fields and sliders if auto mode is enabled
const usedFontSizeDisplay = document.getElementById('usedFontSizeDisplay');
const usedPaddingDisplay = document.getElementById('usedPaddingDisplay');
const usedFontDisplay = document.getElementById('usedFontDisplay');
if (fontSizeUsed && fontSizeAuto.checked) {
fontSizeInput.value = fontSizeUsed;
fontSizeSlider.value = fontSizeUsed;
usedFontSizeDisplay.textContent = `Using: ${fontSizeUsed}px`;
usedFontSizeDisplay.style.display = 'block';
} else {
usedFontSizeDisplay.style.display = 'none';
}
if (paddingUsed && paddingAuto.checked) {
paddingInput.value = paddingUsed;
paddingSlider.value = paddingUsed;
usedPaddingDisplay.textContent = `Using: ${paddingUsed}px`;
usedPaddingDisplay.style.display = 'block';
} else {
usedPaddingDisplay.style.display = 'none';
}
if (fontNameUsed && fontSelect.value === 'auto') {
usedFontDisplay.textContent = `Using: ${fontNameUsed}`;
usedFontDisplay.style.display = 'block';
} else {
usedFontDisplay.style.display = 'none';
}
// Set image and wait for it to load before hiding spinner
previewImage.onload = () => {
previewImage.classList.remove('loading');
previewLoadingOverlay.classList.remove('active');
};
previewImage.src = url;
previewImage.dataset.downloadUrl = url;
previewImage.dataset.downloadName = `logo_${text.replace(/\s+/g, '_')}.png`;
preview.style.display = 'block';
// Store the actual values used for dimension overlay
previewImage.dataset.fontSize = fontSize;
previewImage.dataset.padding = padding;
previewImage.dataset.position = position;
// Store original dimensions if available
if (originalImageWidth && originalImageHeight) {
previewImage.dataset.originalWidth = originalImageWidth;
previewImage.dataset.originalHeight = originalImageHeight;
} else {
// Fallback - estimate from processed image natural dimensions
// This is approximate but better than nothing
previewImage.dataset.originalWidth = previewImage.naturalWidth;
previewImage.dataset.originalHeight = previewImage.naturalHeight;
}
preview.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
// Update dimension overlay after image loads
setTimeout(updateDimensionOverlay, 100);
} catch (error) {
showMessage(`❌ Error: ${error.message}`, 'error');
previewImage.classList.remove('loading');
previewLoadingOverlay.classList.remove('active');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = '✨ Generate Logo';
}
});
// Live preview functionality (always enabled)
let generateTimeout = null;
function triggerLivePreview() {
clearTimeout(generateTimeout);
generateTimeout = setTimeout(() => {
// Trigger form submission programmatically
const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
form.dispatchEvent(submitEvent);
}, 800); // 800ms debounce
}
// Add live preview listeners to all form inputs
document.getElementById('text').addEventListener('input', triggerLivePreview);
document.querySelectorAll('input[name="position"]').forEach(radio => {
radio.addEventListener('change', triggerLivePreview);
});
fontSizeInput.addEventListener('input', triggerLivePreview);
fontSizeSlider.addEventListener('input', triggerLivePreview);
fontSizeAuto.addEventListener('change', triggerLivePreview);
paddingInput.addEventListener('input', triggerLivePreview);
paddingSlider.addEventListener('input', triggerLivePreview);
paddingAuto.addEventListener('change', triggerLivePreview);
textColorInput.addEventListener('input', triggerLivePreview);
textColorPicker.addEventListener('input', triggerLivePreview);
bgColorInput.addEventListener('input', triggerLivePreview);
bgColorPicker.addEventListener('input', triggerLivePreview);
fontSelect.addEventListener('change', triggerLivePreview);
imageUrl.addEventListener('input', triggerLivePreview);
selectedLogoUrl.addEventListener('change', triggerLivePreview);
// Trigger on file selection
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>