import gradio as gr import qrcode import random import os from datetime import datetime from PIL import Image, ImageDraw from math import cos, sin, radians def create_border_decoration(qr_image, decoration_style="Flowers"): # Convert QR image to RGB mode first qr_image = qr_image.convert('RGB') # Get the size of the QR code image width, height = qr_image.size # 패딩을 더 작게 조정 padding = 20 new_width = width + (padding * 2) new_height = height + (padding * 2) # Create new image with white background decorated_image = Image.new('RGB', (new_width, new_height), 'white') # Paste QR code in center decorated_image.paste(qr_image, (padding, padding)) # Get draw object draw = ImageDraw.Draw(decorated_image) # 장식 크기 설정 deco_size = 8 gap = deco_size * 1.5 # 테두리를 따라 점들의 위치 계산 border_points = [] # 상단 테두리 for x in range(padding//2, new_width - padding//2, int(gap)): border_points.append((x, padding//2)) # 우측 테두리 for y in range(padding//2, new_height - padding//2, int(gap)): border_points.append((new_width - padding//2, y)) # 하단 테두리 for x in range(new_width - padding//2, padding//2, -int(gap)): border_points.append((x, new_height - padding//2)) # 좌측 테두리 for y in range(new_height - padding//2, padding//2, -int(gap)): border_points.append((padding//2, y)) # 각 스타일에 따른 장식 그리기 for x, y in border_points: if decoration_style == "Flowers": for angle in range(0, 360, 45): x1 = x + deco_size * cos(radians(angle)) y1 = y + deco_size * sin(radians(angle)) draw.ellipse([x1-4, y1-4, x1+4, y1+4], fill='pink') draw.ellipse([x-3, y-3, x+3, y+3], fill='yellow') elif decoration_style == "Hearts": heart_size = 6 draw.ellipse([x-heart_size, y-heart_size, x, y], fill='red') draw.ellipse([x, y-heart_size, x+heart_size, y], fill='red') draw.polygon([(x-heart_size, y), (x+heart_size, y), (x, y+heart_size)], fill='red') elif decoration_style == "Waves": wave_size = 10 draw.arc([x-wave_size, y-wave_size//2, x+wave_size, y+wave_size//2], 0, 180, fill='lightblue', width=2) draw.arc([x-wave_size, y, x+wave_size, y+wave_size], 0, 180, fill='blue', width=2) elif decoration_style == "Leaves": leaf_size = 8 draw.ellipse([x-leaf_size, y-leaf_size//2, x+leaf_size, y+leaf_size//2], fill='lightgreen') draw.ellipse([x-leaf_size//2, y-leaf_size, x+leaf_size//2, y+leaf_size], fill='darkgreen') elif decoration_style == "Stars": star_size = 6 points = [] for i in range(5): angle = i * 72 x1 = x + star_size * cos(radians(angle)) y1 = y + star_size * sin(radians(angle)) points.append((x1, y1)) x2 = x + (star_size/2) * cos(radians(angle + 36)) y2 = y + (star_size/2) * sin(radians(angle + 36)) points.append((x2, y2)) draw.polygon(points, fill='gold') elif decoration_style == "Chains": chain_size = 8 draw.ellipse([x-chain_size, y-chain_size//2, x+chain_size, y+chain_size//2], outline='gray', width=2) elif decoration_style == "Bubbles": bubble_sizes = [6, 4, 2] for size in bubble_sizes: draw.ellipse([x-size, y-size, x+size, y+size], outline='skyblue', width=1) elif decoration_style == "Vines": vine_size = 10 draw.arc([x-vine_size, y-vine_size, x+vine_size, y+vine_size], 0, 180, fill='green', width=2) draw.ellipse([x-3, y-3, x+3, y+3], fill='lightgreen') elif decoration_style == "Diamonds": diamond_size = 6 points = [ (x, y-diamond_size), (x+diamond_size, y), (x, y+diamond_size), (x-diamond_size, y) ] draw.polygon(points, outline='purple', width=1) elif decoration_style == "Lace": lace_size = 8 draw.arc([x-lace_size, y-lace_size, x+lace_size, y+lace_size], 0, 180, fill='gray', width=1) draw.arc([x-lace_size//2, y-lace_size//2, x+lace_size//2, y+lace_size//2], 180, 360, fill='gray', width=1) return decorated_image def rgba_to_rgb(rgba_color): """Convert RGBA color string to RGB hex color""" if rgba_color.startswith('rgba'): values = rgba_color.strip('rgba()').split(',') r = int(float(values[0])) g = int(float(values[1])) b = int(float(values[2])) return f'#{r:02x}{g:02x}{b:02x}' return rgba_color def create_qr(content, qr_type, fill_color, back_color, box_size, border_size, error_correction, border_decoration="No Decoration"): # Convert RGBA colors to RGB fill_color = rgba_to_rgb(fill_color) back_color = rgba_to_rgb(back_color) # QR 코드 데이터 포맷팅 formatted_data = format_data(content, qr_type) # 에러 수정 레벨 설정 error_levels = { "Low (7%)": qrcode.constants.ERROR_CORRECT_L, "Medium (15%)": qrcode.constants.ERROR_CORRECT_M, "Quartile (25%)": qrcode.constants.ERROR_CORRECT_Q, "High (30%)": qrcode.constants.ERROR_CORRECT_H } # QR 코드 생성 qr = qrcode.QRCode( version=1, error_correction=error_levels[error_correction], box_size=box_size, border=border_size, ) qr.add_data(formatted_data) qr.make(fit=True) # QR 이미지 생성 qr_img = qr.make_image(fill_color=fill_color, back_color=back_color) # Add border decoration if specified if border_decoration != "No Decoration": qr_img = create_border_decoration(qr_img, border_decoration) # 파일 저장 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") random_id = random.randint(1000, 9999) filename = f"qrfile/qr_{timestamp}_{random_id}.png" os.makedirs("qrfile", exist_ok=True) qr_img.save(filename) cleanup_old_files("qrfile/", max_files=100) # 정보 로그 생성 info_log = f"""✅ QR CODE GENERATED! {'=' * 50} 📋 QR Code Info: • Type: {qr_type} • Content: {content[:40]}{'...' if len(content) > 40 else ''} {'=' * 50} 🎨 Style Settings: • QR Color: {fill_color} • Background: {back_color} • Size: {box_size} • Border: {border_size} • Decoration: {border_decoration} {'=' * 50} 🔧 Technical: • Error Correction: {error_correction} • Formatted Data: {formatted_data[:50]}{'...' if len(formatted_data) > 50 else ''} {'=' * 50} 💾 Ready to download!""" return filename, formatted_data, info_log def format_data(content, qr_type): if not content: return "" format_rules = { "URL": lambda x: f"https://{x}" if not x.startswith(('http://', 'https://')) else x, "Email": lambda x: f"mailto:{x}", "Phone": lambda x: f"tel:{x}", "SMS": lambda x: f"sms:{x}", "WhatsApp": lambda x: f"whatsapp://send?text={x}", "Location": lambda x: f"geo:{x}", "Wi-Fi": lambda x: f"WIFI:S:{x};;", "Text": lambda x: x, "vCard": lambda x: f"BEGIN:VCARD\nVERSION:3.0\n{x}\nEND:VCARD" } return format_rules[qr_type](content.strip()) def cleanup_old_files(directory, max_files): files = [f for f in os.listdir(directory) if f.endswith('.png')] if len(files) > max_files: files.sort(key=lambda x: os.path.getctime(os.path.join(directory, x))) for f in files[:-max_files]: try: os.remove(os.path.join(directory, f)) except: continue def format_example_text(qr_type): examples = { "URL": "• Direct URL: https://example.com\n• Without https: example.com", "Email": "• Basic: example@email.com\n• With subject: example@email.com?subject=Hello", "Phone": "• International: +1234567890\n• Local: 01012345678", "SMS": "• Basic: +1234567890\n• With message: +1234567890?body=Hello", "WhatsApp": "• Message: Hello World!\n• With number: +1234567890:Hello", "Location": "• Coordinates: 37.7749,-122.4194\n• With zoom: 37.7749,-122.4194,15z", "Wi-Fi": "• Network name only: MyWiFiNetwork\n• With password: WIFI:S:MyNetwork;P:password;;", "Text": "• Simple text: Hello World!\n• Multiple lines: Line 1\\nLine 2", "vCard": "• Basic:\nFN:John Doe\nTEL:+1234567890\nEMAIL:john@example.com" } return examples.get(qr_type, "Enter your content here...") # ============================================ # 🎨 Comic Classic Theme - Toon Playground # ============================================ css = """ /* ===== 🎨 Google Fonts Import ===== */ @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap'); /* ===== 🎨 Comic Classic 배경 - 빈티지 페이퍼 + 도트 패턴 ===== */ .gradio-container { background-color: #FEF9C3 !important; background-image: radial-gradient(#1F2937 1px, transparent 1px) !important; background-size: 20px 20px !important; min-height: 100vh !important; font-family: 'Comic Neue', cursive, sans-serif !important; } /* ===== 허깅페이스 상단 요소 숨김 ===== */ .huggingface-space-header, #space-header, .space-header, [class*="space-header"], .svelte-1ed2p3z, .space-header-badge, .header-badge, [data-testid="space-header"], .svelte-kqij2n, .svelte-1ax1toq, .embed-container > div:first-child { display: none !important; visibility: hidden !important; height: 0 !important; width: 0 !important; overflow: hidden !important; opacity: 0 !important; pointer-events: none !important; } /* ===== Footer 완전 숨김 ===== */ footer, .footer, .gradio-container footer, .built-with, [class*="footer"], .gradio-footer, .main-footer, div[class*="footer"], .show-api, .built-with-gradio, a[href*="gradio.app"], a[href*="huggingface.co/spaces"] { display: none !important; visibility: hidden !important; height: 0 !important; padding: 0 !important; margin: 0 !important; } /* ===== 메인 컨테이너 ===== */ #col-container { max-width: 1200px; margin: 0 auto; } /* ===== 🎨 헤더 타이틀 - 코믹 스타일 ===== */ .header-text h1 { font-family: 'Bangers', cursive !important; color: #1F2937 !important; font-size: 3.5rem !important; font-weight: 400 !important; text-align: center !important; margin-bottom: 0.5rem !important; text-shadow: 4px 4px 0px #FACC15, 6px 6px 0px #1F2937 !important; letter-spacing: 3px !important; -webkit-text-stroke: 2px #1F2937 !important; } /* ===== 🎨 서브타이틀 ===== */ .subtitle { text-align: center !important; font-family: 'Comic Neue', cursive !important; font-size: 1.2rem !important; color: #1F2937 !important; margin-bottom: 1.5rem !important; font-weight: 700 !important; } /* ===== 🎨 카드/패널 - 만화 프레임 스타일 ===== */ .gr-panel, .gr-box, .gr-form, .block, .gr-group { background: #FFFFFF !important; border: 3px solid #1F2937 !important; border-radius: 8px !important; box-shadow: 6px 6px 0px #1F2937 !important; transition: all 0.2s ease !important; } .gr-panel:hover, .block:hover { transform: translate(-2px, -2px) !important; box-shadow: 8px 8px 0px #1F2937 !important; } /* ===== 🎨 입력 필드 (Textbox) ===== */ textarea, input[type="text"], input[type="number"] { background: #FFFFFF !important; border: 3px solid #1F2937 !important; border-radius: 8px !important; color: #1F2937 !important; font-family: 'Comic Neue', cursive !important; font-size: 1rem !important; font-weight: 700 !important; transition: all 0.2s ease !important; } textarea:focus, input[type="text"]:focus, input[type="number"]:focus { border-color: #3B82F6 !important; box-shadow: 4px 4px 0px #3B82F6 !important; outline: none !important; } textarea::placeholder { color: #9CA3AF !important; font-weight: 400 !important; } /* ===== 🎨 드롭다운 스타일 ===== */ .gr-dropdown { background: #FFFFFF !important; border: 3px solid #1F2937 !important; border-radius: 8px !important; box-shadow: 3px 3px 0px #1F2937 !important; } .gr-dropdown > div { background: #FFFFFF !important; border: none !important; } .gr-dropdown input { color: #1F2937 !important; font-family: 'Comic Neue', cursive !important; font-weight: 700 !important; } .gr-dropdown ul { background: #FFFFFF !important; border: 3px solid #1F2937 !important; border-radius: 8px !important; box-shadow: 4px 4px 0px #1F2937 !important; } .gr-dropdown ul li { color: #1F2937 !important; font-family: 'Comic Neue', cursive !important; font-weight: 700 !important; padding: 8px 12px !important; } .gr-dropdown ul li:hover { background: #FACC15 !important; color: #1F2937 !important; } .gr-dropdown ul li.selected { background: #3B82F6 !important; color: #FFFFFF !important; } /* ===== 🎨 Primary 버튼 - 코믹 블루 ===== */ .gr-button-primary, button.primary, .gr-button.primary, .generate-btn { background: #3B82F6 !important; border: 3px solid #1F2937 !important; border-radius: 8px !important; color: #FFFFFF !important; font-family: 'Bangers', cursive !important; font-weight: 400 !important; font-size: 1.3rem !important; letter-spacing: 2px !important; padding: 14px 28px !important; box-shadow: 5px 5px 0px #1F2937 !important; transition: all 0.1s ease !important; text-shadow: 1px 1px 0px #1F2937 !important; } .gr-button-primary:hover, button.primary:hover, .gr-button.primary:hover, .generate-btn:hover { background: #2563EB !important; transform: translate(-2px, -2px) !important; box-shadow: 7px 7px 0px #1F2937 !important; } .gr-button-primary:active, button.primary:active, .gr-button.primary:active, .generate-btn:active { transform: translate(3px, 3px) !important; box-shadow: 2px 2px 0px #1F2937 !important; } /* ===== 🎨 Secondary 버튼 - 코믹 레드 ===== */ .gr-button-secondary, button.secondary { background: #EF4444 !important; border: 3px solid #1F2937 !important; border-radius: 8px !important; color: #FFFFFF !important; font-family: 'Bangers', cursive !important; font-weight: 400 !important; font-size: 1.1rem !important; letter-spacing: 1px !important; box-shadow: 4px 4px 0px #1F2937 !important; transition: all 0.1s ease !important; text-shadow: 1px 1px 0px #1F2937 !important; } .gr-button-secondary:hover, button.secondary:hover { background: #DC2626 !important; transform: translate(-2px, -2px) !important; box-shadow: 6px 6px 0px #1F2937 !important; } /* ===== 🎨 로그 출력 영역 ===== */ .info-log textarea { background: #1F2937 !important; color: #10B981 !important; font-family: 'Courier New', monospace !important; font-size: 0.9rem !important; font-weight: 400 !important; border: 3px solid #10B981 !important; border-radius: 8px !important; box-shadow: 4px 4px 0px #10B981 !important; } /* ===== 🎨 컬러 피커 스타일 ===== */ .gr-colorpicker, input[type="color"] { border: 3px solid #1F2937 !important; border-radius: 8px !important; box-shadow: 3px 3px 0px #1F2937 !important; width: 60px !important; height: 40px !important; cursor: pointer !important; } .gr-colorpicker:hover, input[type="color"]:hover { transform: translate(-2px, -2px) !important; box-shadow: 5px 5px 0px #1F2937 !important; } /* ===== 🎨 슬라이더 스타일 ===== */ input[type="range"] { accent-color: #3B82F6 !important; height: 8px !important; } .gr-slider { background: #FFFFFF !important; } /* ===== 🎨 예시 텍스트 박스 ===== */ .example-box textarea { background: #FEF3C7 !important; border: 2px dashed #F59E0B !important; border-radius: 8px !important; color: #92400E !important; font-family: 'Comic Neue', cursive !important; font-size: 0.9rem !important; } /* ===== 🎨 QR 코드 출력 영역 ===== */ .qr-output, .gr-image { border: 4px solid #1F2937 !important; border-radius: 8px !important; box-shadow: 8px 8px 0px #1F2937 !important; overflow: hidden !important; background: #FFFFFF !important; } /* ===== 🎨 아코디언 - 말풍선 스타일 ===== */ .gr-accordion { background: #FACC15 !important; border: 3px solid #1F2937 !important; border-radius: 8px !important; box-shadow: 4px 4px 0px #1F2937 !important; } .gr-accordion-header { color: #1F2937 !important; font-family: 'Comic Neue', cursive !important; font-weight: 700 !important; font-size: 1.1rem !important; } /* ===== 🎨 라벨 스타일 ===== */ label, .gr-input-label, .gr-block-label { color: #1F2937 !important; font-family: 'Comic Neue', cursive !important; font-weight: 700 !important; font-size: 1rem !important; } span.gr-label { color: #1F2937 !important; } /* ===== 🎨 정보 텍스트 ===== */ .gr-info, .info { color: #6B7280 !important; font-family: 'Comic Neue', cursive !important; font-size: 0.9rem !important; } /* ===== 🎨 Instructions 섹션 ===== */ .instructions-box { background: linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%) !important; border: 3px solid #3B82F6 !important; border-radius: 12px !important; padding: 1.5rem !important; box-shadow: 6px 6px 0px #1F2937 !important; margin-top: 1.5rem !important; } .instructions-box h3 { font-family: 'Bangers', cursive !important; color: #1F2937 !important; font-size: 1.3rem !important; margin-bottom: 0.5rem !important; } .instructions-box ul, .instructions-box ol { font-family: 'Comic Neue', cursive !important; color: #1F2937 !important; font-weight: 700 !important; } /* ===== 🎨 Tips 섹션 ===== */ .tips-box { background: linear-gradient(135deg, #FEF3C7 0%, #FDE68A 100%) !important; border: 3px solid #F59E0B !important; border-radius: 12px !important; padding: 1.5rem !important; box-shadow: 6px 6px 0px #1F2937 !important; margin-top: 1rem !important; } /* ===== 🎨 스크롤바 - 코믹 스타일 ===== */ ::-webkit-scrollbar { width: 12px; height: 12px; } ::-webkit-scrollbar-track { background: #FEF9C3; border: 2px solid #1F2937; } ::-webkit-scrollbar-thumb { background: #3B82F6; border: 2px solid #1F2937; border-radius: 0px; } ::-webkit-scrollbar-thumb:hover { background: #EF4444; } /* ===== 🎨 선택 하이라이트 ===== */ ::selection { background: #FACC15; color: #1F2937; } /* ===== 🎨 링크 스타일 ===== */ a { color: #3B82F6 !important; text-decoration: none !important; font-weight: 700 !important; } a:hover { color: #EF4444 !important; } /* ===== 🎨 Row/Column 간격 ===== */ .gr-row { gap: 1.5rem !important; } .gr-column { gap: 1rem !important; } /* ===== 🎨 컬러 피커 Row ===== */ .color-picker-row { display: flex !important; gap: 1rem !important; align-items: center !important; } /* ===== 반응형 조정 ===== */ @media (max-width: 768px) { .header-text h1 { font-size: 2.2rem !important; text-shadow: 3px 3px 0px #FACC15, 4px 4px 0px #1F2937 !important; } .gr-button-primary, button.primary { padding: 12px 20px !important; font-size: 1.1rem !important; } .gr-panel, .block { box-shadow: 4px 4px 0px #1F2937 !important; } } /* ===== 🎨 다크모드 비활성화 ===== */ @media (prefers-color-scheme: dark) { .gradio-container { background-color: #FEF9C3 !important; } } """ def create_interface(): with gr.Blocks(fill_height=True, css=css, title="QR Canvas+") as demo: # HOME Badge gr.HTML("""
HOME
""") # Header Title gr.Markdown( """ # 🎯 QR CANVAS+ GENERATOR 📱 """, elem_classes="header-text" ) gr.Markdown( """

🎨 Create customized QR codes with professional styling & decorations! ✨

""", ) with gr.Row(equal_height=False): # Left column - Input Settings with gr.Column(scale=2, min_width=450): qr_type = gr.Dropdown( choices=["URL", "Email", "Phone", "SMS", "WhatsApp", "Location", "Wi-Fi", "Text", "vCard"], value="URL", label="📋 QR Code Type" ) content = gr.Textbox( label="✏️ Content", placeholder="Enter your content here...", lines=3 ) example_format = gr.Textbox( value=format_example_text("URL"), label="📝 Format Examples", interactive=False, lines=4, elem_classes="example-box" ) with gr.Row(elem_classes="color-picker-row"): fill_color = gr.ColorPicker( label="🎨 QR Code Color", value="#000000" ) back_color = gr.ColorPicker( label="🖼️ Background Color", value="#FFFFFF" ) with gr.Row(): box_size = gr.Slider( minimum=1, maximum=30, value=15, step=1, label="📐 QR Code Size" ) border_size = gr.Slider( minimum=0, maximum=5, value=2, step=1, label="🔲 Border Size" ) error_correction = gr.Dropdown( choices=[ "Low (7%)", "Medium (15%)", "Quartile (25%)", "High (30%)" ], value="Medium (15%)", label="🔧 Error Correction Level" ) border_decoration = gr.Dropdown( choices=[ "No Decoration", "Flowers", "Hearts", "Waves", "Leaves", "Stars", "Chains", "Bubbles", "Vines", "Diamonds", "Lace" ], value="No Decoration", label="🌸 Border Decoration Style" ) generate_btn = gr.Button( "🎯 GENERATE QR CODE! 📱", variant="primary", size="lg", elem_classes="generate-btn" ) with gr.Accordion("📜 Generation Log", open=True): info_log = gr.Textbox( label="", placeholder="Enter content and click generate to see info...", lines=14, max_lines=20, interactive=False, elem_classes="info-log" ) # Right column - Output with gr.Column(scale=1, min_width=350): output_image = gr.Image( label="🖼️ Generated QR Code", type="filepath", height=400, elem_classes="qr-output" ) output_data = gr.Textbox( label="📄 Formatted Data", interactive=False, lines=2 ) gr.Markdown( """

💡 Right-click on the QR code to save!

""" ) # Instructions Section gr.Markdown( """

📝 INSTRUCTIONS

  1. Select the QR code type from the dropdown menu
  2. Enter your content following the format examples shown
  3. Customize the appearance using the color pickers and sliders
  4. Choose a border decoration style (optional)
  5. Click 'GENERATE QR CODE' to create your custom QR code

💡 TIPS

""" ) # Event handlers def update_example(qr_type): return format_example_text(qr_type) qr_type.change( fn=update_example, inputs=[qr_type], outputs=example_format ) generate_btn.click( fn=create_qr, inputs=[ content, qr_type, fill_color, back_color, box_size, border_size, error_correction, border_decoration ], outputs=[output_image, output_data, info_log] ) return demo if __name__ == "__main__": try: os.makedirs("qrfile", exist_ok=True) demo = create_interface() demo.launch( server_name="0.0.0.0", server_port=7860, share=True, debug=True ) except Exception as e: print(f"Error starting the application: {e}")