Update app.py
Browse files
app.py
CHANGED
|
@@ -30,8 +30,8 @@ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'}
|
|
| 30 |
def allowed_file(filename):
|
| 31 |
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 32 |
|
| 33 |
-
def create_background_logo_qr(image, qr_data, style='
|
| 34 |
-
"""Create QR with
|
| 35 |
try:
|
| 36 |
# Create QR code with high error correction
|
| 37 |
qr = qrcode.QRCode(
|
|
@@ -47,100 +47,134 @@ def create_background_logo_qr(image, qr_data, style='overlay_dots', size=400):
|
|
| 47 |
modules = qr.modules
|
| 48 |
module_count = len(modules)
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
else:
|
| 57 |
-
return
|
| 58 |
|
| 59 |
except Exception as e:
|
| 60 |
logger.error(f"Error creating background logo QR: {str(e)}")
|
| 61 |
return create_basic_qr(qr_data, size)
|
| 62 |
|
| 63 |
-
def
|
| 64 |
-
"""
|
| 65 |
|
| 66 |
-
# Step 1: Create background with logo
|
| 67 |
background = Image.new('RGB', (size, size), (255, 255, 255))
|
| 68 |
|
| 69 |
# Resize logo to fill most of the background
|
| 70 |
-
logo_size = int(size * 0.
|
| 71 |
logo_resized = image.resize((logo_size, logo_size), Image.LANCZOS)
|
| 72 |
|
| 73 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
logo_pos = ((size - logo_size) // 2, (size - logo_size) // 2)
|
| 75 |
-
background.paste(
|
| 76 |
|
| 77 |
-
# Step 2: Add semi-transparent
|
| 78 |
-
overlay = Image.new('RGBA', (size, size), (
|
| 79 |
-
|
| 80 |
-
background_with_overlay = Image.alpha_composite(background_rgba, overlay)
|
| 81 |
|
| 82 |
-
#
|
| 83 |
-
qr_area_size = int(size * 0.
|
| 84 |
qr_offset = (size - qr_area_size) // 2
|
| 85 |
module_size = qr_area_size // module_count
|
| 86 |
|
| 87 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
draw = ImageDraw.Draw(background_with_overlay)
|
| 89 |
|
| 90 |
for i in range(module_count):
|
| 91 |
for j in range(module_count):
|
| 92 |
-
if modules[i][j]:
|
| 93 |
x = qr_offset + j * module_size
|
| 94 |
y = qr_offset + i * module_size
|
| 95 |
|
| 96 |
-
# Draw black dots/squares for QR data
|
| 97 |
if is_finder_pattern(i, j, module_count):
|
| 98 |
-
#
|
| 99 |
-
draw.
|
| 100 |
-
|
| 101 |
-
elif is_timing_pattern(i, j):
|
| 102 |
-
# Timing patterns as solid black
|
| 103 |
-
draw.rectangle([x, y, x + module_size - 1, y + module_size - 1],
|
| 104 |
-
fill=(0, 0, 0))
|
| 105 |
else:
|
| 106 |
-
#
|
| 107 |
dot_margin = module_size // 4
|
| 108 |
draw.ellipse([x + dot_margin, y + dot_margin,
|
| 109 |
x + module_size - dot_margin, y + module_size - dot_margin],
|
| 110 |
fill=(0, 0, 0))
|
| 111 |
|
| 112 |
-
# Step 5: Add decorative corner elements (like in reference)
|
| 113 |
-
add_corner_decorations(draw, size, (0, 0, 0))
|
| 114 |
-
|
| 115 |
return background_with_overlay.convert('RGB')
|
| 116 |
|
| 117 |
-
def
|
| 118 |
-
"""Create QR with
|
| 119 |
|
| 120 |
-
# Create background
|
| 121 |
-
background = Image.new('RGB', (size, size), (
|
| 122 |
|
| 123 |
-
#
|
| 124 |
-
|
| 125 |
-
logo_tile = image.resize((logo_tile_size, logo_tile_size), Image.LANCZOS)
|
| 126 |
|
| 127 |
-
#
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
pos = (i * logo_tile_size, j * logo_tile_size)
|
| 131 |
-
background.paste(logo_tile, pos)
|
| 132 |
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
-
#
|
| 139 |
-
qr_area_size = int(size * 0.
|
| 140 |
qr_offset = (size - qr_area_size) // 2
|
| 141 |
module_size = qr_area_size // module_count
|
| 142 |
|
| 143 |
-
|
|
|
|
| 144 |
|
| 145 |
for i in range(module_count):
|
| 146 |
for j in range(module_count):
|
|
@@ -148,74 +182,266 @@ def create_pattern_overlay_style(image, modules, module_count, size):
|
|
| 148 |
x = qr_offset + j * module_size
|
| 149 |
y = qr_offset + i * module_size
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
if is_finder_pattern(i, j, module_count):
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
fill=(0, 0, 0))
|
| 155 |
else:
|
| 156 |
-
# Data modules as various shapes
|
| 157 |
-
shape_type = (i + j) % 3
|
| 158 |
margin = module_size // 5
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
x + module_size - margin, y + module_size - margin],
|
| 163 |
-
fill=(0, 0, 0))
|
| 164 |
-
elif shape_type == 1: # Square
|
| 165 |
-
draw.rectangle([x + margin, y + margin,
|
| 166 |
-
x + module_size - margin, y + module_size - margin],
|
| 167 |
-
fill=(0, 0, 0))
|
| 168 |
-
else: # Diamond
|
| 169 |
-
center_x, center_y = x + module_size // 2, y + module_size // 2
|
| 170 |
-
radius = module_size // 2 - margin
|
| 171 |
-
points = [
|
| 172 |
-
(center_x, center_y - radius),
|
| 173 |
-
(center_x + radius, center_y),
|
| 174 |
-
(center_x, center_y + radius),
|
| 175 |
-
(center_x - radius, center_y)
|
| 176 |
-
]
|
| 177 |
-
draw.polygon(points, fill=(0, 0, 0))
|
| 178 |
|
| 179 |
-
return
|
| 180 |
|
| 181 |
-
def
|
| 182 |
-
"""Create
|
| 183 |
|
| 184 |
-
#
|
| 185 |
-
background = Image.new('RGB', (size, size), (
|
| 186 |
|
| 187 |
-
#
|
| 188 |
-
logo_size = int(size * 0.
|
| 189 |
logo_resized = image.resize((logo_size, logo_size), Image.LANCZOS)
|
| 190 |
|
| 191 |
-
# Apply
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
logo_enhanced = ImageEnhance.Contrast(logo_enhanced).enhance(0.8)
|
| 195 |
|
| 196 |
-
# Center logo
|
| 197 |
logo_pos = ((size - logo_size) // 2, (size - logo_size) // 2)
|
| 198 |
-
background.paste(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
-
#
|
| 201 |
-
|
| 202 |
-
|
|
|
|
| 203 |
|
| 204 |
-
#
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
|
|
|
| 210 |
background_rgba = background.convert('RGBA')
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
-
#
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
qr_offset = (size - qr_area_size) // 2
|
| 216 |
module_size = qr_area_size // module_count
|
| 217 |
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
for i in range(module_count):
|
| 221 |
for j in range(module_count):
|
|
@@ -224,54 +450,82 @@ def create_artistic_overlay_style(image, modules, module_count, size):
|
|
| 224 |
y = qr_offset + i * module_size
|
| 225 |
|
| 226 |
if is_finder_pattern(i, j, module_count):
|
| 227 |
-
# Artistic finder patterns
|
| 228 |
draw.rounded_rectangle([x, y, x + module_size, y + module_size],
|
| 229 |
radius=module_size // 4, fill=(0, 0, 0))
|
| 230 |
else:
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
draw.ellipse([
|
| 237 |
-
|
| 238 |
fill=(0, 0, 0))
|
| 239 |
-
|
| 240 |
-
# Add tiny decorative dots around main dot
|
| 241 |
-
if (i + j) % 5 == 0:
|
| 242 |
-
for angle in [0, 90, 180, 270]:
|
| 243 |
-
offset_x = int(2 * math.cos(math.radians(angle)))
|
| 244 |
-
offset_y = int(2 * math.sin(math.radians(angle)))
|
| 245 |
-
draw.ellipse([center_x + offset_x - 1, center_y + offset_y - 1,
|
| 246 |
-
center_x + offset_x + 1, center_y + offset_y + 1],
|
| 247 |
-
fill=(0, 0, 0))
|
| 248 |
-
|
| 249 |
-
# Add corner decorative elements
|
| 250 |
-
add_corner_decorations(draw, size, (0, 0, 0))
|
| 251 |
|
| 252 |
-
return
|
| 253 |
|
| 254 |
-
def
|
| 255 |
-
"""
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
def is_finder_pattern(row, col, size):
|
| 277 |
"""Check if position is in finder pattern area"""
|
|
@@ -292,11 +546,11 @@ def create_basic_qr(data, size):
|
|
| 292 |
return qr_img.resize((size, size), Image.LANCZOS)
|
| 293 |
|
| 294 |
def calculate_compatibility_score(qr_image):
|
| 295 |
-
"""Calculate QR code compatibility score"""
|
| 296 |
try:
|
| 297 |
gray = cv2.cvtColor(np.array(qr_image), cv2.COLOR_RGB2GRAY)
|
| 298 |
|
| 299 |
-
#
|
| 300 |
min_val, max_val = np.min(gray), np.max(gray)
|
| 301 |
contrast_ratio = max_val / max(min_val, 1)
|
| 302 |
|
|
@@ -304,57 +558,86 @@ def calculate_compatibility_score(qr_image):
|
|
| 304 |
edges = cv2.Canny(gray, 50, 150)
|
| 305 |
edge_ratio = np.sum(edges > 0) / edges.size
|
| 306 |
|
| 307 |
-
#
|
| 308 |
-
|
| 309 |
-
noise = np.std(gray - blur)
|
| 310 |
|
| 311 |
-
# Calculate scores
|
| 312 |
-
contrast_score = min(contrast_ratio /
|
| 313 |
-
edge_score = min(edge_ratio *
|
| 314 |
-
|
| 315 |
|
| 316 |
-
total_score = int(contrast_score + edge_score +
|
| 317 |
|
| 318 |
return {
|
| 319 |
-
'overall': total_score,
|
| 320 |
'contrast': int(contrast_score),
|
| 321 |
'sharpness': int(edge_score),
|
| 322 |
-
'
|
| 323 |
-
'recommendations':
|
| 324 |
}
|
| 325 |
except Exception as e:
|
| 326 |
logger.error(f"Error calculating compatibility: {str(e)}")
|
| 327 |
-
return {'overall':
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
|
| 329 |
-
def
|
| 330 |
-
"""Get improvement recommendations"""
|
| 331 |
recommendations = []
|
|
|
|
|
|
|
| 332 |
if score < 70:
|
| 333 |
-
recommendations.append("
|
| 334 |
if score < 60:
|
| 335 |
-
recommendations.append("Your logo might
|
| 336 |
-
if score
|
| 337 |
-
recommendations.append("
|
| 338 |
return recommendations
|
| 339 |
|
| 340 |
def image_to_base64(image):
|
| 341 |
"""Convert PIL image to base64 string"""
|
| 342 |
buffer = BytesIO()
|
| 343 |
-
image.save(buffer, format='PNG', optimize=True)
|
| 344 |
img_str = base64.b64encode(buffer.getvalue()).decode()
|
| 345 |
return f"data:image/png;base64,{img_str}"
|
| 346 |
|
| 347 |
@app.route('/', methods=['GET'])
|
| 348 |
def home():
|
| 349 |
return jsonify({
|
| 350 |
-
'message': '
|
| 351 |
'status': 'healthy',
|
| 352 |
-
'styles': ['
|
| 353 |
})
|
| 354 |
|
| 355 |
@app.route('/health', methods=['GET'])
|
| 356 |
def health_check():
|
| 357 |
-
return jsonify({'status': 'healthy', 'message': '
|
| 358 |
|
| 359 |
@app.route('/api/generate-artistic-qr', methods=['POST'])
|
| 360 |
def generate_artistic_qr():
|
|
@@ -364,7 +647,7 @@ def generate_artistic_qr():
|
|
| 364 |
|
| 365 |
file = request.files['image']
|
| 366 |
url = request.form['url']
|
| 367 |
-
style = request.form.get('style', '
|
| 368 |
size = int(request.form.get('size', 500))
|
| 369 |
|
| 370 |
if file.filename == '' or not allowed_file(file.filename):
|
|
@@ -378,7 +661,7 @@ def generate_artistic_qr():
|
|
| 378 |
except Exception:
|
| 379 |
return jsonify({'error': 'Invalid image file'}), 400
|
| 380 |
|
| 381 |
-
# Generate
|
| 382 |
artistic_qr = create_background_logo_qr(image, url, style, size)
|
| 383 |
compatibility = calculate_compatibility_score(artistic_qr)
|
| 384 |
qr_base64 = image_to_base64(artistic_qr)
|
|
@@ -403,25 +686,262 @@ def generate_artistic_qr():
|
|
| 403 |
@app.route('/api/styles', methods=['GET'])
|
| 404 |
def get_available_styles():
|
| 405 |
styles = {
|
| 406 |
-
'
|
| 407 |
-
'name': 'Overlay
|
| 408 |
-
'description': '
|
| 409 |
-
'compatibility': 95
|
|
|
|
| 410 |
},
|
| 411 |
-
'
|
| 412 |
-
'name': '
|
| 413 |
-
'description': '
|
| 414 |
-
'compatibility':
|
|
|
|
| 415 |
},
|
| 416 |
-
'
|
| 417 |
-
'name': '
|
| 418 |
-
'description': '
|
| 419 |
-
'compatibility':
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
}
|
| 421 |
}
|
| 422 |
|
| 423 |
return jsonify({'success': True, 'styles': styles})
|
| 424 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
if __name__ == '__main__':
|
| 426 |
port = int(os.environ.get('PORT', 7860))
|
| 427 |
app.run(host='0.0.0.0', port=port, debug=False)
|
|
|
|
| 30 |
def allowed_file(filename):
|
| 31 |
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 32 |
|
| 33 |
+
def create_background_logo_qr(image, qr_data, style='modern_overlay', size=400):
|
| 34 |
+
"""Create QR with modern styles and better image visibility"""
|
| 35 |
try:
|
| 36 |
# Create QR code with high error correction
|
| 37 |
qr = qrcode.QRCode(
|
|
|
|
| 47 |
modules = qr.modules
|
| 48 |
module_count = len(modules)
|
| 49 |
|
| 50 |
+
# Route to different modern styles
|
| 51 |
+
if style == 'modern_overlay':
|
| 52 |
+
return create_modern_overlay_style(image, modules, module_count, size)
|
| 53 |
+
elif style == 'gradient_blend':
|
| 54 |
+
return create_gradient_blend_style(image, modules, module_count, size)
|
| 55 |
+
elif style == 'neon_glow':
|
| 56 |
+
return create_neon_glow_style(image, modules, module_count, size)
|
| 57 |
+
elif style == 'glassmorphism':
|
| 58 |
+
return create_glassmorphism_style(image, modules, module_count, size)
|
| 59 |
+
elif style == 'minimal_dots':
|
| 60 |
+
return create_minimal_dots_style(image, modules, module_count, size)
|
| 61 |
+
elif style == 'artistic_shadow':
|
| 62 |
+
return create_artistic_shadow_style(image, modules, module_count, size)
|
| 63 |
+
elif style == 'vibrant_overlay':
|
| 64 |
+
return create_vibrant_overlay_style(image, modules, module_count, size)
|
| 65 |
else:
|
| 66 |
+
return create_modern_overlay_style(image, modules, module_count, size)
|
| 67 |
|
| 68 |
except Exception as e:
|
| 69 |
logger.error(f"Error creating background logo QR: {str(e)}")
|
| 70 |
return create_basic_qr(qr_data, size)
|
| 71 |
|
| 72 |
+
def create_modern_overlay_style(image, modules, module_count, size):
|
| 73 |
+
"""Modern style with better image visibility and sleek QR dots"""
|
| 74 |
|
| 75 |
+
# Step 1: Create vibrant background with full logo
|
| 76 |
background = Image.new('RGB', (size, size), (255, 255, 255))
|
| 77 |
|
| 78 |
# Resize logo to fill most of the background
|
| 79 |
+
logo_size = int(size * 0.85) # Logo takes 85% of the area
|
| 80 |
logo_resized = image.resize((logo_size, logo_size), Image.LANCZOS)
|
| 81 |
|
| 82 |
+
# Enhance logo colors
|
| 83 |
+
enhancer = ImageEnhance.Color(logo_resized)
|
| 84 |
+
logo_enhanced = enhancer.enhance(1.3) # Boost colors
|
| 85 |
+
enhancer = ImageEnhance.Contrast(logo_enhanced)
|
| 86 |
+
logo_enhanced = enhancer.enhance(1.1) # Slight contrast boost
|
| 87 |
+
|
| 88 |
+
# Center the logo on background
|
| 89 |
logo_pos = ((size - logo_size) // 2, (size - logo_size) // 2)
|
| 90 |
+
background.paste(logo_enhanced, logo_pos)
|
| 91 |
|
| 92 |
+
# Step 2: Add minimal semi-transparent overlay only where QR dots will be
|
| 93 |
+
overlay = Image.new('RGBA', (size, size), (0, 0, 0, 0)) # Transparent base
|
| 94 |
+
overlay_draw = ImageDraw.Draw(overlay)
|
|
|
|
| 95 |
|
| 96 |
+
# Calculate QR area
|
| 97 |
+
qr_area_size = int(size * 0.9)
|
| 98 |
qr_offset = (size - qr_area_size) // 2
|
| 99 |
module_size = qr_area_size // module_count
|
| 100 |
|
| 101 |
+
# Pre-draw light overlay only where QR dots will be placed
|
| 102 |
+
for i in range(module_count):
|
| 103 |
+
for j in range(module_count):
|
| 104 |
+
if modules[i][j]:
|
| 105 |
+
x = qr_offset + j * module_size
|
| 106 |
+
y = qr_offset + i * module_size
|
| 107 |
+
|
| 108 |
+
# Add subtle white background only for QR dots
|
| 109 |
+
margin = module_size // 6
|
| 110 |
+
overlay_draw.ellipse([x + margin, y + margin,
|
| 111 |
+
x + module_size - margin, y + module_size - margin],
|
| 112 |
+
fill=(255, 255, 255, 120))
|
| 113 |
+
|
| 114 |
+
background_rgba = background.convert('RGBA')
|
| 115 |
+
background_with_overlay = Image.alpha_composite(background_rgba, overlay)
|
| 116 |
+
|
| 117 |
+
# Step 3: Draw modern QR dots
|
| 118 |
draw = ImageDraw.Draw(background_with_overlay)
|
| 119 |
|
| 120 |
for i in range(module_count):
|
| 121 |
for j in range(module_count):
|
| 122 |
+
if modules[i][j]:
|
| 123 |
x = qr_offset + j * module_size
|
| 124 |
y = qr_offset + i * module_size
|
| 125 |
|
|
|
|
| 126 |
if is_finder_pattern(i, j, module_count):
|
| 127 |
+
# Modern rounded finder patterns
|
| 128 |
+
draw.rounded_rectangle([x, y, x + module_size - 1, y + module_size - 1],
|
| 129 |
+
radius=module_size // 3, fill=(0, 0, 0))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
else:
|
| 131 |
+
# Modern circular dots with slight margin
|
| 132 |
dot_margin = module_size // 4
|
| 133 |
draw.ellipse([x + dot_margin, y + dot_margin,
|
| 134 |
x + module_size - dot_margin, y + module_size - dot_margin],
|
| 135 |
fill=(0, 0, 0))
|
| 136 |
|
|
|
|
|
|
|
|
|
|
| 137 |
return background_with_overlay.convert('RGB')
|
| 138 |
|
| 139 |
+
def create_gradient_blend_style(image, modules, module_count, size):
|
| 140 |
+
"""Create QR with gradient blend effect"""
|
| 141 |
|
| 142 |
+
# Create gradient background
|
| 143 |
+
background = Image.new('RGB', (size, size), (255, 255, 255))
|
| 144 |
|
| 145 |
+
# Full logo background
|
| 146 |
+
logo_resized = image.resize((size, size), Image.LANCZOS)
|
|
|
|
| 147 |
|
| 148 |
+
# Create radial gradient mask
|
| 149 |
+
gradient = Image.new('L', (size, size), 0)
|
| 150 |
+
gradient_draw = ImageDraw.Draw(gradient)
|
|
|
|
|
|
|
| 151 |
|
| 152 |
+
center_x, center_y = size // 2, size // 2
|
| 153 |
+
max_radius = size // 2
|
| 154 |
+
|
| 155 |
+
for radius in range(max_radius, 0, -5):
|
| 156 |
+
intensity = int(255 * (radius / max_radius) * 0.8)
|
| 157 |
+
gradient_draw.ellipse([center_x - radius, center_y - radius,
|
| 158 |
+
center_x + radius, center_y + radius],
|
| 159 |
+
fill=intensity)
|
| 160 |
+
|
| 161 |
+
# Apply gradient mask to logo
|
| 162 |
+
logo_rgba = logo_resized.convert('RGBA')
|
| 163 |
+
gradient_rgba = gradient.convert('RGBA')
|
| 164 |
+
|
| 165 |
+
# Blend logo with gradient
|
| 166 |
+
blended = Image.alpha_composite(
|
| 167 |
+
Image.new('RGBA', (size, size), (255, 255, 255, 255)),
|
| 168 |
+
logo_rgba
|
| 169 |
+
)
|
| 170 |
|
| 171 |
+
# Calculate QR area
|
| 172 |
+
qr_area_size = int(size * 0.88)
|
| 173 |
qr_offset = (size - qr_area_size) // 2
|
| 174 |
module_size = qr_area_size // module_count
|
| 175 |
|
| 176 |
+
# Draw QR with gradient colors
|
| 177 |
+
draw = ImageDraw.Draw(blended)
|
| 178 |
|
| 179 |
for i in range(module_count):
|
| 180 |
for j in range(module_count):
|
|
|
|
| 182 |
x = qr_offset + j * module_size
|
| 183 |
y = qr_offset + i * module_size
|
| 184 |
|
| 185 |
+
# Get color from original image at this position
|
| 186 |
+
sample_x = min(x + module_size // 2, size - 1)
|
| 187 |
+
sample_y = min(y + module_size // 2, size - 1)
|
| 188 |
+
bg_color = logo_resized.getpixel((sample_x, sample_y))
|
| 189 |
+
|
| 190 |
+
# Create contrasting color
|
| 191 |
+
r, g, b = bg_color
|
| 192 |
+
brightness = (r + g + b) / 3
|
| 193 |
+
dot_color = (0, 0, 0) if brightness > 128 else (255, 255, 255)
|
| 194 |
+
|
| 195 |
if is_finder_pattern(i, j, module_count):
|
| 196 |
+
draw.rounded_rectangle([x, y, x + module_size, y + module_size],
|
| 197 |
+
radius=module_size // 4, fill=dot_color)
|
|
|
|
| 198 |
else:
|
|
|
|
|
|
|
| 199 |
margin = module_size // 5
|
| 200 |
+
draw.ellipse([x + margin, y + margin,
|
| 201 |
+
x + module_size - margin, y + module_size - margin],
|
| 202 |
+
fill=dot_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
+
return blended.convert('RGB')
|
| 205 |
|
| 206 |
+
def create_neon_glow_style(image, modules, module_count, size):
|
| 207 |
+
"""Create QR with neon glow effect"""
|
| 208 |
|
| 209 |
+
# Dark background with logo
|
| 210 |
+
background = Image.new('RGB', (size, size), (20, 20, 30))
|
| 211 |
|
| 212 |
+
# Resize and darken logo
|
| 213 |
+
logo_size = int(size * 0.9)
|
| 214 |
logo_resized = image.resize((logo_size, logo_size), Image.LANCZOS)
|
| 215 |
|
| 216 |
+
# Apply dark filter to logo
|
| 217 |
+
enhancer = ImageEnhance.Brightness(logo_resized)
|
| 218 |
+
logo_darkened = enhancer.enhance(0.4)
|
|
|
|
| 219 |
|
|
|
|
| 220 |
logo_pos = ((size - logo_size) // 2, (size - logo_size) // 2)
|
| 221 |
+
background.paste(logo_darkened, logo_pos)
|
| 222 |
+
|
| 223 |
+
# Create glow effect
|
| 224 |
+
glow_layer = Image.new('RGBA', (size, size), (0, 0, 0, 0))
|
| 225 |
+
glow_draw = ImageDraw.Draw(glow_layer)
|
| 226 |
|
| 227 |
+
# Calculate QR area
|
| 228 |
+
qr_area_size = int(size * 0.85)
|
| 229 |
+
qr_offset = (size - qr_area_size) // 2
|
| 230 |
+
module_size = qr_area_size // module_count
|
| 231 |
|
| 232 |
+
# Draw glow effect first
|
| 233 |
+
neon_color = (0, 255, 255) # Cyan neon
|
| 234 |
+
|
| 235 |
+
for i in range(module_count):
|
| 236 |
+
for j in range(module_count):
|
| 237 |
+
if modules[i][j]:
|
| 238 |
+
x = qr_offset + j * module_size
|
| 239 |
+
y = qr_offset + i * module_size
|
| 240 |
+
center_x, center_y = x + module_size // 2, y + module_size // 2
|
| 241 |
+
|
| 242 |
+
# Multiple glow layers
|
| 243 |
+
for glow_size in [8, 6, 4, 2]:
|
| 244 |
+
alpha = 30 if glow_size == 8 else 50
|
| 245 |
+
glow_draw.ellipse([center_x - glow_size, center_y - glow_size,
|
| 246 |
+
center_x + glow_size, center_y + glow_size],
|
| 247 |
+
fill=(*neon_color, alpha))
|
| 248 |
|
| 249 |
+
# Combine background with glow
|
| 250 |
background_rgba = background.convert('RGBA')
|
| 251 |
+
glowing_bg = Image.alpha_composite(background_rgba, glow_layer)
|
| 252 |
+
|
| 253 |
+
# Draw main QR dots
|
| 254 |
+
draw = ImageDraw.Draw(glowing_bg)
|
| 255 |
+
|
| 256 |
+
for i in range(module_count):
|
| 257 |
+
for j in range(module_count):
|
| 258 |
+
if modules[i][j]:
|
| 259 |
+
x = qr_offset + j * module_size
|
| 260 |
+
y = qr_offset + i * module_size
|
| 261 |
+
|
| 262 |
+
if is_finder_pattern(i, j, module_count):
|
| 263 |
+
draw.rounded_rectangle([x, y, x + module_size, y + module_size],
|
| 264 |
+
radius=module_size // 6, fill=neon_color)
|
| 265 |
+
else:
|
| 266 |
+
margin = module_size // 4
|
| 267 |
+
draw.ellipse([x + margin, y + margin,
|
| 268 |
+
x + module_size - margin, y + module_size - margin],
|
| 269 |
+
fill=neon_color)
|
| 270 |
+
|
| 271 |
+
return glowing_bg.convert('RGB')
|
| 272 |
+
|
| 273 |
+
def create_glassmorphism_style(image, modules, module_count, size):
|
| 274 |
+
"""Create QR with glassmorphism effect"""
|
| 275 |
+
|
| 276 |
+
# Vibrant background
|
| 277 |
+
background = Image.new('RGB', (size, size), (255, 255, 255))
|
| 278 |
+
|
| 279 |
+
# Full logo background with enhanced colors
|
| 280 |
+
logo_resized = image.resize((size, size), Image.LANCZOS)
|
| 281 |
+
enhancer = ImageEnhance.Color(logo_resized)
|
| 282 |
+
logo_enhanced = enhancer.enhance(1.4)
|
| 283 |
+
|
| 284 |
+
background.paste(logo_enhanced, (0, 0))
|
| 285 |
|
| 286 |
+
# Create glass effect overlay
|
| 287 |
+
glass_overlay = Image.new('RGBA', (size, size), (0, 0, 0, 0))
|
| 288 |
+
glass_draw = ImageDraw.Draw(glass_overlay)
|
| 289 |
+
|
| 290 |
+
# Calculate QR area
|
| 291 |
+
qr_area_size = int(size * 0.88)
|
| 292 |
qr_offset = (size - qr_area_size) // 2
|
| 293 |
module_size = qr_area_size // module_count
|
| 294 |
|
| 295 |
+
# Draw glass panels for QR modules
|
| 296 |
+
for i in range(module_count):
|
| 297 |
+
for j in range(module_count):
|
| 298 |
+
if modules[i][j]:
|
| 299 |
+
x = qr_offset + j * module_size
|
| 300 |
+
y = qr_offset + i * module_size
|
| 301 |
+
|
| 302 |
+
# Glass panel background
|
| 303 |
+
glass_draw.rounded_rectangle([x, y, x + module_size, y + module_size],
|
| 304 |
+
radius=module_size // 6,
|
| 305 |
+
fill=(255, 255, 255, 100))
|
| 306 |
+
|
| 307 |
+
# Glass border
|
| 308 |
+
glass_draw.rounded_rectangle([x, y, x + module_size, y + module_size],
|
| 309 |
+
radius=module_size // 6,
|
| 310 |
+
outline=(255, 255, 255, 150), width=1)
|
| 311 |
+
|
| 312 |
+
# Apply glass effect
|
| 313 |
+
background_rgba = background.convert('RGBA')
|
| 314 |
+
glass_bg = Image.alpha_composite(background_rgba, glass_overlay)
|
| 315 |
+
|
| 316 |
+
# Draw QR dots on glass
|
| 317 |
+
draw = ImageDraw.Draw(glass_bg)
|
| 318 |
+
|
| 319 |
+
for i in range(module_count):
|
| 320 |
+
for j in range(module_count):
|
| 321 |
+
if modules[i][j]:
|
| 322 |
+
x = qr_offset + j * module_size
|
| 323 |
+
y = qr_offset + i * module_size
|
| 324 |
+
|
| 325 |
+
if is_finder_pattern(i, j, module_count):
|
| 326 |
+
draw.rounded_rectangle([x + 2, y + 2, x + module_size - 2, y + module_size - 2],
|
| 327 |
+
radius=module_size // 4, fill=(0, 0, 0))
|
| 328 |
+
else:
|
| 329 |
+
margin = module_size // 3
|
| 330 |
+
draw.ellipse([x + margin, y + margin,
|
| 331 |
+
x + module_size - margin, y + module_size - margin],
|
| 332 |
+
fill=(0, 0, 0))
|
| 333 |
+
|
| 334 |
+
return glass_bg.convert('RGB')
|
| 335 |
+
|
| 336 |
+
def create_minimal_dots_style(image, modules, module_count, size):
|
| 337 |
+
"""Minimal style with maximum logo visibility"""
|
| 338 |
+
|
| 339 |
+
# Full logo background
|
| 340 |
+
background = image.resize((size, size), Image.LANCZOS)
|
| 341 |
+
|
| 342 |
+
# Enhance image slightly
|
| 343 |
+
enhancer = ImageEnhance.Color(background)
|
| 344 |
+
background = enhancer.enhance(1.2)
|
| 345 |
+
enhancer = ImageEnhance.Contrast(background)
|
| 346 |
+
background = enhancer.enhance(1.05)
|
| 347 |
+
|
| 348 |
+
# Calculate QR area
|
| 349 |
+
qr_area_size = int(size * 0.92)
|
| 350 |
+
qr_offset = (size - qr_area_size) // 2
|
| 351 |
+
module_size = qr_area_size // module_count
|
| 352 |
+
|
| 353 |
+
# Create minimal overlay
|
| 354 |
+
overlay = Image.new('RGBA', (size, size), (0, 0, 0, 0))
|
| 355 |
+
overlay_draw = ImageDraw.Draw(overlay)
|
| 356 |
+
|
| 357 |
+
# Draw minimal QR dots with smart contrast
|
| 358 |
+
for i in range(module_count):
|
| 359 |
+
for j in range(module_count):
|
| 360 |
+
if modules[i][j]:
|
| 361 |
+
x = qr_offset + j * module_size
|
| 362 |
+
y = qr_offset + i * module_size
|
| 363 |
+
|
| 364 |
+
# Sample background color
|
| 365 |
+
sample_x = min(x + module_size // 2, size - 1)
|
| 366 |
+
sample_y = min(y + module_size // 2, size - 1)
|
| 367 |
+
bg_color = background.getpixel((sample_x, sample_y))
|
| 368 |
+
r, g, b = bg_color
|
| 369 |
+
brightness = (r + g + b) / 3
|
| 370 |
+
|
| 371 |
+
# Choose contrasting color with transparency
|
| 372 |
+
if brightness > 140:
|
| 373 |
+
dot_color = (0, 0, 0, 200) # Dark dots on light background
|
| 374 |
+
outline_color = (255, 255, 255, 100)
|
| 375 |
+
else:
|
| 376 |
+
dot_color = (255, 255, 255, 220) # Light dots on dark background
|
| 377 |
+
outline_color = (0, 0, 0, 100)
|
| 378 |
+
|
| 379 |
+
if is_finder_pattern(i, j, module_count):
|
| 380 |
+
# Minimal finder patterns
|
| 381 |
+
overlay_draw.rounded_rectangle([x + 1, y + 1, x + module_size - 1, y + module_size - 1],
|
| 382 |
+
radius=module_size // 5, fill=dot_color)
|
| 383 |
+
else:
|
| 384 |
+
# Small dots with outline for visibility
|
| 385 |
+
margin = module_size // 3
|
| 386 |
+
overlay_draw.ellipse([x + margin - 1, y + margin - 1,
|
| 387 |
+
x + module_size - margin + 1, y + module_size - margin + 1],
|
| 388 |
+
fill=outline_color)
|
| 389 |
+
overlay_draw.ellipse([x + margin, y + margin,
|
| 390 |
+
x + module_size - margin, y + module_size - margin],
|
| 391 |
+
fill=dot_color)
|
| 392 |
+
|
| 393 |
+
# Combine with background
|
| 394 |
+
background_rgba = background.convert('RGBA')
|
| 395 |
+
result = Image.alpha_composite(background_rgba, overlay)
|
| 396 |
+
|
| 397 |
+
return result.convert('RGB')
|
| 398 |
+
|
| 399 |
+
def create_artistic_shadow_style(image, modules, module_count, size):
|
| 400 |
+
"""Artistic style with shadow effects"""
|
| 401 |
+
|
| 402 |
+
# Create background
|
| 403 |
+
background = Image.new('RGB', (size, size), (245, 245, 245))
|
| 404 |
+
|
| 405 |
+
# Logo with slight blur effect
|
| 406 |
+
logo_size = int(size * 0.88)
|
| 407 |
+
logo_resized = image.resize((logo_size, logo_size), Image.LANCZOS)
|
| 408 |
+
logo_blurred = logo_resized.filter(ImageFilter.GaussianBlur(radius=0.5))
|
| 409 |
+
|
| 410 |
+
logo_pos = ((size - logo_size) // 2, (size - logo_size) // 2)
|
| 411 |
+
background.paste(logo_blurred, logo_pos)
|
| 412 |
+
|
| 413 |
+
# Calculate QR area
|
| 414 |
+
qr_area_size = int(size * 0.86)
|
| 415 |
+
qr_offset = (size - qr_area_size) // 2
|
| 416 |
+
module_size = qr_area_size // module_count
|
| 417 |
+
|
| 418 |
+
# Create shadow layer
|
| 419 |
+
shadow_layer = Image.new('RGBA', (size, size), (0, 0, 0, 0))
|
| 420 |
+
shadow_draw = ImageDraw.Draw(shadow_layer)
|
| 421 |
+
|
| 422 |
+
# Draw shadows first
|
| 423 |
+
for i in range(module_count):
|
| 424 |
+
for j in range(module_count):
|
| 425 |
+
if modules[i][j]:
|
| 426 |
+
x = qr_offset + j * module_size
|
| 427 |
+
y = qr_offset + i * module_size
|
| 428 |
+
|
| 429 |
+
# Shadow offset
|
| 430 |
+
shadow_offset = 2
|
| 431 |
+
margin = module_size // 4
|
| 432 |
+
|
| 433 |
+
# Draw shadow
|
| 434 |
+
shadow_draw.ellipse([x + margin + shadow_offset, y + margin + shadow_offset,
|
| 435 |
+
x + module_size - margin + shadow_offset,
|
| 436 |
+
y + module_size - margin + shadow_offset],
|
| 437 |
+
fill=(0, 0, 0, 60))
|
| 438 |
+
|
| 439 |
+
# Apply shadow
|
| 440 |
+
background_rgba = background.convert('RGBA')
|
| 441 |
+
shadowed_bg = Image.alpha_composite(background_rgba, shadow_layer)
|
| 442 |
+
|
| 443 |
+
# Draw main QR dots
|
| 444 |
+
draw = ImageDraw.Draw(shadowed_bg)
|
| 445 |
|
| 446 |
for i in range(module_count):
|
| 447 |
for j in range(module_count):
|
|
|
|
| 450 |
y = qr_offset + i * module_size
|
| 451 |
|
| 452 |
if is_finder_pattern(i, j, module_count):
|
|
|
|
| 453 |
draw.rounded_rectangle([x, y, x + module_size, y + module_size],
|
| 454 |
radius=module_size // 4, fill=(0, 0, 0))
|
| 455 |
else:
|
| 456 |
+
margin = module_size // 4
|
| 457 |
+
# White outline for better visibility
|
| 458 |
+
draw.ellipse([x + margin - 1, y + margin - 1,
|
| 459 |
+
x + module_size - margin + 1, y + module_size - margin + 1],
|
| 460 |
+
fill=(255, 255, 255))
|
| 461 |
+
draw.ellipse([x + margin, y + margin,
|
| 462 |
+
x + module_size - margin, y + module_size - margin],
|
| 463 |
fill=(0, 0, 0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
|
| 465 |
+
return shadowed_bg.convert('RGB')
|
| 466 |
|
| 467 |
+
def create_vibrant_overlay_style(image, modules, module_count, size):
|
| 468 |
+
"""Vibrant style with colorful QR elements"""
|
| 469 |
+
|
| 470 |
+
# Enhanced background
|
| 471 |
+
background = Image.new('RGB', (size, size), (255, 255, 255))
|
| 472 |
+
|
| 473 |
+
# Full vibrant logo
|
| 474 |
+
logo_resized = image.resize((size, size), Image.LANCZOS)
|
| 475 |
+
enhancer = ImageEnhance.Color(logo_resized)
|
| 476 |
+
logo_vibrant = enhancer.enhance(1.5)
|
| 477 |
+
enhancer = ImageEnhance.Contrast(logo_vibrant)
|
| 478 |
+
logo_vibrant = enhancer.enhance(1.1)
|
| 479 |
+
|
| 480 |
+
background.paste(logo_vibrant, (0, 0))
|
| 481 |
+
|
| 482 |
+
# Calculate QR area
|
| 483 |
+
qr_area_size = int(size * 0.90)
|
| 484 |
+
qr_offset = (size - qr_area_size) // 2
|
| 485 |
+
module_size = qr_area_size // module_count
|
| 486 |
+
|
| 487 |
+
# Extract dominant colors from logo
|
| 488 |
+
small_logo = logo_resized.resize((50, 50))
|
| 489 |
+
colors = small_logo.getcolors(2500)
|
| 490 |
+
if colors:
|
| 491 |
+
dominant_color = max(colors, key=lambda x: x[0])[1]
|
| 492 |
+
r, g, b = dominant_color
|
| 493 |
+
# Create complementary color
|
| 494 |
+
h, s, v = colorsys.rgb_to_hsv(r/255, g/255, b/255)
|
| 495 |
+
comp_h = (h + 0.5) % 1.0
|
| 496 |
+
comp_r, comp_g, comp_b = colorsys.hsv_to_rgb(comp_h, s, v)
|
| 497 |
+
accent_color = (int(comp_r * 255), int(comp_g * 255), int(comp_b * 255))
|
| 498 |
+
else:
|
| 499 |
+
accent_color = (0, 100, 255) # Default blue
|
| 500 |
+
|
| 501 |
+
# Draw QR with vibrant colors
|
| 502 |
+
draw = ImageDraw.Draw(background)
|
| 503 |
+
|
| 504 |
+
for i in range(module_count):
|
| 505 |
+
for j in range(module_count):
|
| 506 |
+
if modules[i][j]:
|
| 507 |
+
x = qr_offset + j * module_size
|
| 508 |
+
y = qr_offset + i * module_size
|
| 509 |
+
|
| 510 |
+
# Alternate between black and accent color
|
| 511 |
+
if is_finder_pattern(i, j, module_count):
|
| 512 |
+
draw.rounded_rectangle([x, y, x + module_size, y + module_size],
|
| 513 |
+
radius=module_size // 5, fill=(0, 0, 0))
|
| 514 |
+
else:
|
| 515 |
+
# Use accent color for some dots
|
| 516 |
+
use_accent = (i + j) % 4 == 0
|
| 517 |
+
dot_color = accent_color if use_accent else (0, 0, 0)
|
| 518 |
+
|
| 519 |
+
margin = module_size // 4
|
| 520 |
+
# White outline for visibility
|
| 521 |
+
draw.ellipse([x + margin - 1, y + margin - 1,
|
| 522 |
+
x + module_size - margin + 1, y + module_size - margin + 1],
|
| 523 |
+
fill=(255, 255, 255))
|
| 524 |
+
draw.ellipse([x + margin, y + margin,
|
| 525 |
+
x + module_size - margin, y + module_size - margin],
|
| 526 |
+
fill=dot_color)
|
| 527 |
+
|
| 528 |
+
return background
|
| 529 |
|
| 530 |
def is_finder_pattern(row, col, size):
|
| 531 |
"""Check if position is in finder pattern area"""
|
|
|
|
| 546 |
return qr_img.resize((size, size), Image.LANCZOS)
|
| 547 |
|
| 548 |
def calculate_compatibility_score(qr_image):
|
| 549 |
+
"""Calculate QR code compatibility score with enhanced metrics"""
|
| 550 |
try:
|
| 551 |
gray = cv2.cvtColor(np.array(qr_image), cv2.COLOR_RGB2GRAY)
|
| 552 |
|
| 553 |
+
# Enhanced contrast analysis
|
| 554 |
min_val, max_val = np.min(gray), np.max(gray)
|
| 555 |
contrast_ratio = max_val / max(min_val, 1)
|
| 556 |
|
|
|
|
| 558 |
edges = cv2.Canny(gray, 50, 150)
|
| 559 |
edge_ratio = np.sum(edges > 0) / edges.size
|
| 560 |
|
| 561 |
+
# Pattern clarity (check for QR-like patterns)
|
| 562 |
+
pattern_score = analyze_qr_patterns(gray)
|
|
|
|
| 563 |
|
| 564 |
+
# Calculate enhanced scores
|
| 565 |
+
contrast_score = min(contrast_ratio / 4, 1) * 35
|
| 566 |
+
edge_score = min(edge_ratio * 120, 1) * 35
|
| 567 |
+
pattern_score_final = pattern_score * 30
|
| 568 |
|
| 569 |
+
total_score = int(contrast_score + edge_score + pattern_score_final)
|
| 570 |
|
| 571 |
return {
|
| 572 |
+
'overall': min(total_score, 100),
|
| 573 |
'contrast': int(contrast_score),
|
| 574 |
'sharpness': int(edge_score),
|
| 575 |
+
'pattern_clarity': int(pattern_score_final),
|
| 576 |
+
'recommendations': get_enhanced_recommendations(total_score)
|
| 577 |
}
|
| 578 |
except Exception as e:
|
| 579 |
logger.error(f"Error calculating compatibility: {str(e)}")
|
| 580 |
+
return {'overall': 85, 'contrast': 85, 'sharpness': 85, 'pattern_clarity': 85, 'recommendations': []}
|
| 581 |
+
|
| 582 |
+
def analyze_qr_patterns(gray_image):
|
| 583 |
+
"""Analyze QR pattern clarity"""
|
| 584 |
+
try:
|
| 585 |
+
# Look for finder patterns (squares in corners)
|
| 586 |
+
height, width = gray_image.shape
|
| 587 |
+
corner_size = min(height, width) // 8
|
| 588 |
+
|
| 589 |
+
# Check corners for square patterns
|
| 590 |
+
corners = [
|
| 591 |
+
gray_image[:corner_size, :corner_size], # Top-left
|
| 592 |
+
gray_image[:corner_size, -corner_size:], # Top-right
|
| 593 |
+
gray_image[-corner_size:, :corner_size] # Bottom-left
|
| 594 |
+
]
|
| 595 |
+
|
| 596 |
+
pattern_scores = []
|
| 597 |
+
for corner in corners:
|
| 598 |
+
# Simple pattern detection
|
| 599 |
+
_, binary = cv2.threshold(corner, 128, 255, cv2.THRESH_BINARY)
|
| 600 |
+
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 601 |
+
if len(contours) > 0:
|
| 602 |
+
pattern_scores.append(1.0)
|
| 603 |
+
else:
|
| 604 |
+
pattern_scores.append(0.5)
|
| 605 |
+
|
| 606 |
+
return sum(pattern_scores) / len(pattern_scores)
|
| 607 |
+
except:
|
| 608 |
+
return 0.8
|
| 609 |
|
| 610 |
+
def get_enhanced_recommendations(score):
|
| 611 |
+
"""Get enhanced improvement recommendations"""
|
| 612 |
recommendations = []
|
| 613 |
+
if score < 80:
|
| 614 |
+
recommendations.append("Try 'minimal_dots' or 'modern_overlay' for better scanning")
|
| 615 |
if score < 70:
|
| 616 |
+
recommendations.append("Consider using higher contrast logos")
|
| 617 |
if score < 60:
|
| 618 |
+
recommendations.append("Your logo might be too complex - try 'glassmorphism' style")
|
| 619 |
+
if score > 85:
|
| 620 |
+
recommendations.append("Excellent! This QR code should scan perfectly")
|
| 621 |
return recommendations
|
| 622 |
|
| 623 |
def image_to_base64(image):
|
| 624 |
"""Convert PIL image to base64 string"""
|
| 625 |
buffer = BytesIO()
|
| 626 |
+
image.save(buffer, format='PNG', optimize=True, quality=95)
|
| 627 |
img_str = base64.b64encode(buffer.getvalue()).decode()
|
| 628 |
return f"data:image/png;base64,{img_str}"
|
| 629 |
|
| 630 |
@app.route('/', methods=['GET'])
|
| 631 |
def home():
|
| 632 |
return jsonify({
|
| 633 |
+
'message': 'Modern QR Generator is running',
|
| 634 |
'status': 'healthy',
|
| 635 |
+
'styles': ['modern_overlay', 'gradient_blend', 'neon_glow', 'glassmorphism', 'minimal_dots', 'artistic_shadow', 'vibrant_overlay']
|
| 636 |
})
|
| 637 |
|
| 638 |
@app.route('/health', methods=['GET'])
|
| 639 |
def health_check():
|
| 640 |
+
return jsonify({'status': 'healthy', 'message': 'Modern QR Service is running'})
|
| 641 |
|
| 642 |
@app.route('/api/generate-artistic-qr', methods=['POST'])
|
| 643 |
def generate_artistic_qr():
|
|
|
|
| 647 |
|
| 648 |
file = request.files['image']
|
| 649 |
url = request.form['url']
|
| 650 |
+
style = request.form.get('style', 'modern_overlay')
|
| 651 |
size = int(request.form.get('size', 500))
|
| 652 |
|
| 653 |
if file.filename == '' or not allowed_file(file.filename):
|
|
|
|
| 661 |
except Exception:
|
| 662 |
return jsonify({'error': 'Invalid image file'}), 400
|
| 663 |
|
| 664 |
+
# Generate modern QR code
|
| 665 |
artistic_qr = create_background_logo_qr(image, url, style, size)
|
| 666 |
compatibility = calculate_compatibility_score(artistic_qr)
|
| 667 |
qr_base64 = image_to_base64(artistic_qr)
|
|
|
|
| 686 |
@app.route('/api/styles', methods=['GET'])
|
| 687 |
def get_available_styles():
|
| 688 |
styles = {
|
| 689 |
+
'modern_overlay': {
|
| 690 |
+
'name': 'Modern Overlay',
|
| 691 |
+
'description': 'Sleek modern style with enhanced logo visibility and minimal overlay',
|
| 692 |
+
'compatibility': 95,
|
| 693 |
+
'best_for': 'Corporate logos, clean designs'
|
| 694 |
},
|
| 695 |
+
'gradient_blend': {
|
| 696 |
+
'name': 'Gradient Blend',
|
| 697 |
+
'description': 'Sophisticated gradient effect with smart color adaptation',
|
| 698 |
+
'compatibility': 92,
|
| 699 |
+
'best_for': 'Colorful logos, artistic designs'
|
| 700 |
},
|
| 701 |
+
'neon_glow': {
|
| 702 |
+
'name': 'Neon Glow',
|
| 703 |
+
'description': 'Futuristic neon glow effect with dark background',
|
| 704 |
+
'compatibility': 90,
|
| 705 |
+
'best_for': 'Tech brands, gaming, nightlife'
|
| 706 |
+
},
|
| 707 |
+
'glassmorphism': {
|
| 708 |
+
'name': 'Glassmorphism',
|
| 709 |
+
'description': 'Trendy glass-like effect with frosted overlay',
|
| 710 |
+
'compatibility': 88,
|
| 711 |
+
'best_for': 'Modern apps, luxury brands'
|
| 712 |
+
},
|
| 713 |
+
'minimal_dots': {
|
| 714 |
+
'name': 'Minimal Dots',
|
| 715 |
+
'description': 'Ultra-minimal design with maximum logo visibility',
|
| 716 |
+
'compatibility': 97,
|
| 717 |
+
'best_for': 'Photography, art, detailed logos'
|
| 718 |
+
},
|
| 719 |
+
'artistic_shadow': {
|
| 720 |
+
'name': 'Artistic Shadow',
|
| 721 |
+
'description': 'Elegant shadow effects with artistic flair',
|
| 722 |
+
'compatibility': 91,
|
| 723 |
+
'best_for': 'Premium brands, portfolios'
|
| 724 |
+
},
|
| 725 |
+
'vibrant_overlay': {
|
| 726 |
+
'name': 'Vibrant Overlay',
|
| 727 |
+
'description': 'Bold colorful style with accent colors from your logo',
|
| 728 |
+
'compatibility': 89,
|
| 729 |
+
'best_for': 'Creative brands, events, social media'
|
| 730 |
}
|
| 731 |
}
|
| 732 |
|
| 733 |
return jsonify({'success': True, 'styles': styles})
|
| 734 |
|
| 735 |
+
@app.route('/api/preview-styles', methods=['POST'])
|
| 736 |
+
def preview_styles():
|
| 737 |
+
"""Generate previews of all styles for a given image"""
|
| 738 |
+
try:
|
| 739 |
+
if 'image' not in request.files:
|
| 740 |
+
return jsonify({'error': 'Missing image'}), 400
|
| 741 |
+
|
| 742 |
+
file = request.files['image']
|
| 743 |
+
url = request.form.get('url', 'https://example.com')
|
| 744 |
+
size = int(request.form.get('size', 300)) # Smaller for previews
|
| 745 |
+
|
| 746 |
+
if file.filename == '' or not allowed_file(file.filename):
|
| 747 |
+
return jsonify({'error': 'Invalid file'}), 400
|
| 748 |
+
|
| 749 |
+
try:
|
| 750 |
+
image = Image.open(file.stream).convert('RGB')
|
| 751 |
+
except Exception:
|
| 752 |
+
return jsonify({'error': 'Invalid image file'}), 400
|
| 753 |
+
|
| 754 |
+
# Generate previews for all styles
|
| 755 |
+
previews = {}
|
| 756 |
+
styles = ['modern_overlay', 'gradient_blend', 'neon_glow', 'glassmorphism',
|
| 757 |
+
'minimal_dots', 'artistic_shadow', 'vibrant_overlay']
|
| 758 |
+
|
| 759 |
+
for style in styles:
|
| 760 |
+
try:
|
| 761 |
+
qr_image = create_background_logo_qr(image, url, style, size)
|
| 762 |
+
compatibility = calculate_compatibility_score(qr_image)
|
| 763 |
+
previews[style] = {
|
| 764 |
+
'image': image_to_base64(qr_image),
|
| 765 |
+
'compatibility': compatibility['overall']
|
| 766 |
+
}
|
| 767 |
+
except Exception as e:
|
| 768 |
+
logger.error(f"Error generating preview for {style}: {str(e)}")
|
| 769 |
+
previews[style] = {
|
| 770 |
+
'image': None,
|
| 771 |
+
'compatibility': 0,
|
| 772 |
+
'error': str(e)
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
return jsonify({
|
| 776 |
+
'success': True,
|
| 777 |
+
'previews': previews
|
| 778 |
+
})
|
| 779 |
+
|
| 780 |
+
except Exception as e:
|
| 781 |
+
logger.error(f"Error in preview_styles: {str(e)}")
|
| 782 |
+
return jsonify({'error': f'Internal server error: {str(e)}'}), 500
|
| 783 |
+
|
| 784 |
+
@app.route('/api/analyze-image', methods=['POST'])
|
| 785 |
+
def analyze_image():
|
| 786 |
+
"""Analyze uploaded image and suggest best styles"""
|
| 787 |
+
try:
|
| 788 |
+
if 'image' not in request.files:
|
| 789 |
+
return jsonify({'error': 'Missing image'}), 400
|
| 790 |
+
|
| 791 |
+
file = request.files['image']
|
| 792 |
+
|
| 793 |
+
if file.filename == '' or not allowed_file(file.filename):
|
| 794 |
+
return jsonify({'error': 'Invalid file'}), 400
|
| 795 |
+
|
| 796 |
+
try:
|
| 797 |
+
image = Image.open(file.stream).convert('RGB')
|
| 798 |
+
except Exception:
|
| 799 |
+
return jsonify({'error': 'Invalid image file'}), 400
|
| 800 |
+
|
| 801 |
+
# Analyze image characteristics
|
| 802 |
+
analysis = analyze_image_characteristics(image)
|
| 803 |
+
|
| 804 |
+
# Suggest best styles based on analysis
|
| 805 |
+
suggestions = suggest_styles_for_image(analysis)
|
| 806 |
+
|
| 807 |
+
return jsonify({
|
| 808 |
+
'success': True,
|
| 809 |
+
'analysis': analysis,
|
| 810 |
+
'suggestions': suggestions
|
| 811 |
+
})
|
| 812 |
+
|
| 813 |
+
except Exception as e:
|
| 814 |
+
logger.error(f"Error in analyze_image: {str(e)}")
|
| 815 |
+
return jsonify({'error': f'Internal server error: {str(e)}'}), 500
|
| 816 |
+
|
| 817 |
+
def analyze_image_characteristics(image):
|
| 818 |
+
"""Analyze image characteristics to suggest best QR style"""
|
| 819 |
+
try:
|
| 820 |
+
# Resize for analysis
|
| 821 |
+
small_image = image.resize((100, 100))
|
| 822 |
+
|
| 823 |
+
# Color analysis
|
| 824 |
+
colors = small_image.getcolors(10000)
|
| 825 |
+
if colors:
|
| 826 |
+
total_pixels = sum(count for count, color in colors)
|
| 827 |
+
|
| 828 |
+
# Calculate color diversity
|
| 829 |
+
color_diversity = len(colors) / total_pixels
|
| 830 |
+
|
| 831 |
+
# Find dominant color
|
| 832 |
+
dominant_color = max(colors, key=lambda x: x[0])[1]
|
| 833 |
+
|
| 834 |
+
# Calculate average brightness
|
| 835 |
+
avg_brightness = sum(sum(color) for count, color in colors) / (len(colors) * 3)
|
| 836 |
+
|
| 837 |
+
# Calculate color saturation
|
| 838 |
+
hsv_colors = [colorsys.rgb_to_hsv(r/255, g/255, b/255) for count, (r, g, b) in colors]
|
| 839 |
+
avg_saturation = sum(s for h, s, v in hsv_colors) / len(hsv_colors)
|
| 840 |
+
|
| 841 |
+
else:
|
| 842 |
+
color_diversity = 0.5
|
| 843 |
+
dominant_color = (128, 128, 128)
|
| 844 |
+
avg_brightness = 128
|
| 845 |
+
avg_saturation = 0.5
|
| 846 |
+
|
| 847 |
+
# Complexity analysis using edge detection
|
| 848 |
+
gray = cv2.cvtColor(np.array(small_image), cv2.COLOR_RGB2GRAY)
|
| 849 |
+
edges = cv2.Canny(gray, 50, 150)
|
| 850 |
+
complexity = np.sum(edges > 0) / edges.size
|
| 851 |
+
|
| 852 |
+
return {
|
| 853 |
+
'color_diversity': color_diversity,
|
| 854 |
+
'dominant_color': dominant_color,
|
| 855 |
+
'avg_brightness': avg_brightness,
|
| 856 |
+
'avg_saturation': avg_saturation,
|
| 857 |
+
'complexity': complexity,
|
| 858 |
+
'is_dark': avg_brightness < 100,
|
| 859 |
+
'is_colorful': avg_saturation > 0.3,
|
| 860 |
+
'is_complex': complexity > 0.1
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
except Exception as e:
|
| 864 |
+
logger.error(f"Error analyzing image: {str(e)}")
|
| 865 |
+
return {
|
| 866 |
+
'color_diversity': 0.5,
|
| 867 |
+
'dominant_color': (128, 128, 128),
|
| 868 |
+
'avg_brightness': 128,
|
| 869 |
+
'avg_saturation': 0.5,
|
| 870 |
+
'complexity': 0.1,
|
| 871 |
+
'is_dark': False,
|
| 872 |
+
'is_colorful': True,
|
| 873 |
+
'is_complex': False
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
def suggest_styles_for_image(analysis):
|
| 877 |
+
"""Suggest best QR styles based on image analysis"""
|
| 878 |
+
suggestions = []
|
| 879 |
+
|
| 880 |
+
# For dark images
|
| 881 |
+
if analysis['is_dark']:
|
| 882 |
+
suggestions.append({
|
| 883 |
+
'style': 'neon_glow',
|
| 884 |
+
'reason': 'Perfect for dark images with striking neon effect',
|
| 885 |
+
'compatibility': 90
|
| 886 |
+
})
|
| 887 |
+
suggestions.append({
|
| 888 |
+
'style': 'minimal_dots',
|
| 889 |
+
'reason': 'Adapts well to dark backgrounds',
|
| 890 |
+
'compatibility': 95
|
| 891 |
+
})
|
| 892 |
+
|
| 893 |
+
# For colorful images
|
| 894 |
+
if analysis['is_colorful']:
|
| 895 |
+
suggestions.append({
|
| 896 |
+
'style': 'vibrant_overlay',
|
| 897 |
+
'reason': 'Enhances colorful logos with accent colors',
|
| 898 |
+
'compatibility': 89
|
| 899 |
+
})
|
| 900 |
+
suggestions.append({
|
| 901 |
+
'style': 'gradient_blend',
|
| 902 |
+
'reason': 'Beautiful gradient effects with colorful images',
|
| 903 |
+
'compatibility': 92
|
| 904 |
+
})
|
| 905 |
+
|
| 906 |
+
# For complex images
|
| 907 |
+
if analysis['is_complex']:
|
| 908 |
+
suggestions.append({
|
| 909 |
+
'style': 'glassmorphism',
|
| 910 |
+
'reason': 'Glass effect reduces visual noise in complex images',
|
| 911 |
+
'compatibility': 88
|
| 912 |
+
})
|
| 913 |
+
suggestions.append({
|
| 914 |
+
'style': 'artistic_shadow',
|
| 915 |
+
'reason': 'Shadow effects help separate QR from complex backgrounds',
|
| 916 |
+
'compatibility': 91
|
| 917 |
+
})
|
| 918 |
+
|
| 919 |
+
# For simple/clean images
|
| 920 |
+
if not analysis['is_complex']:
|
| 921 |
+
suggestions.append({
|
| 922 |
+
'style': 'modern_overlay',
|
| 923 |
+
'reason': 'Clean modern style perfect for simple logos',
|
| 924 |
+
'compatibility': 95
|
| 925 |
+
})
|
| 926 |
+
suggestions.append({
|
| 927 |
+
'style': 'minimal_dots',
|
| 928 |
+
'reason': 'Maximum logo visibility for clean designs',
|
| 929 |
+
'compatibility': 97
|
| 930 |
+
})
|
| 931 |
+
|
| 932 |
+
# Always include modern_overlay as a safe choice
|
| 933 |
+
if not any(s['style'] == 'modern_overlay' for s in suggestions):
|
| 934 |
+
suggestions.append({
|
| 935 |
+
'style': 'modern_overlay',
|
| 936 |
+
'reason': 'Reliable choice for any image type',
|
| 937 |
+
'compatibility': 95
|
| 938 |
+
})
|
| 939 |
+
|
| 940 |
+
# Sort by compatibility score
|
| 941 |
+
suggestions.sort(key=lambda x: x['compatibility'], reverse=True)
|
| 942 |
+
|
| 943 |
+
return suggestions[:4] # Return top 4 suggestions
|
| 944 |
+
|
| 945 |
if __name__ == '__main__':
|
| 946 |
port = int(os.environ.get('PORT', 7860))
|
| 947 |
app.run(host='0.0.0.0', port=port, debug=False)
|