diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a7dd101e --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +``` +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Dependencies +.venv/ +venv/ +env/ +.env +.env.local +.env.* + +# Logs and temp files +*.log +*.tmp + +# Editors +.vscode/ +.idea/ +*.swp +*.swo + +# Build and distribution +dist/ +build/ +*.egg-info/ + +# Coverage +.coverage +coverage/ +htmlcov/ + +# Testing +.pytest_cache/ +.mypy_cache/ +``` \ No newline at end of file diff --git a/python/README_GUI.md b/python/README_GUI.md new file mode 100644 index 00000000..85504a21 --- /dev/null +++ b/python/README_GUI.md @@ -0,0 +1,170 @@ +# QR Code Generator GUI - Hướng dẫn sử dụng + +## Tổng quan + +Ứng dụng GUI tạo mã QR với các tính năng: +- ✅ **4 ngôn ngữ**: English, Tiếng Việt, 日本語,中文 +- ✅ **2 chủ đề**: Light Theme & Dark Theme +- ✅ **Xuất SVG** với tùy chỉnh màu sắc và tỷ lệ +- ✅ **Giao diện hiện đại**, dễ sử dụng + +## Yêu cầu hệ thống + +```bash +# Python 3.6+ +# Tkinter (thường đi kèm với Python) +``` + +## Cài đặt + +### Trên Ubuntu/Debian: +```bash +sudo apt-get install python3-tk +``` + +### Trên Fedora: +```bash +sudo dnf install python3-tkinter +``` + +### Trên macOS: +```bash +brew install python-tk +``` + +### Trên Windows: +Tkinter thường được cài đặt sẵn với Python. + +## Chạy ứng dụng + +```bash +cd /workspace/python +python qrcodegen_gui.py +``` + +## Tính năng chính + +### 1. Đa ngôn ngữ (4 languages) +- **English** - Tiếng Anh +- **Tiếng Việt** - Vietnamese +- **日本語** - Japanese +- **中文** - Chinese + +Chuyển đổi ngôn ngữ qua menu `Language` hoặc phím tắt. + +### 2. Chủ đề sáng/tối (Light/Dark Theme) +- **Light Theme**: Giao diện sáng, phù hợp ban ngày +- **Dark Theme**: Giao diện tối, bảo vệ mắt, phù hợp ban đêm + +Chuyển đổi qua: +- Menu `View > Light/Dark Theme` +- Nút toggle góc phải màn hình +- Phím tắt + +### 3. Tùy chỉnh mã QR +- **Error Correction**: L, M, Q, H (từ thấp đến cao) +- **Border**: Độ rộng viền (0-20 modules) +- **Scale**: Tỷ lệ phóng to (1-50 pixels/module) +- **Foreground Color**: Màu前景 (mặc định: đen) +- **Background Color**: Màu nền (mặc định: trắng) + +### 4. Xuất file +- **Save as SVG**: Lưu file vector SVG +- **Copy SVG**: Sao chép mã SVG vào clipboard +- **Clear**: Xóa toàn bộ để tạo mới + +## Cấu trúc code + +``` +qrcodegen_gui.py +├── LANGUAGES # Dictionary chứa bản dịch 4 ngôn ngữ +├── LIGHT_THEME # Cấu hình màu light theme +├── DARK_THEME # Cấu hình màu dark theme +└── QRCodeGenerator # Class chính + ├── __init__() # Khởi tạo giao diện + ├── _create_menu() # Tạo menu bar + ├── _create_widgets() # Tạo widgets + ├── _apply_theme() # Áp dụng theme + ├── _update_language() # Cập nhật ngôn ngữ + ├── generate_qr() # Tạo mã QR + ├── save_svg() # Lưu file SVG + └── ... +``` + +## API Usage + +### Tạo QR Code với SVG: + +```python +from qrcodegen import QrCode +from qrcodegen_gui import QRCodeGenerator + +# Tạo instance +generator = QRCodeGenerator(root=None) # root=None cho headless mode + +# Tạo QR Code +qr = QrCode.encode_text("Hello, World!", QrCode.Ecc.MEDIUM) + +# Tạo SVG string +svg = generator.to_svg_str( + qr, + border=4, + scale=10, + foreground_color="#000080", # Xanh dương + background_color="#FFFFFF" # Trắng +) + +# Lưu vào file +with open("qrcode.svg", "w") as f: + f.write(svg) +``` + +## Screenshots + +### Light Theme (English) +- Giao diện sáng, sạch sẽ +- Menu đầy đủ chức năng +- Preview QR Code realtime + +### Dark Theme (Tiếng Việt) +- Giao diện tối, dịu mắt +- Full tiếng Việt +- Tương phản tốt + +## Phím tắt + +| Phím | Chức năng | +|------|-----------| +| `Ctrl+S` | Lưu file SVG | +| `Ctrl+Q` | Thoát ứng dụng | + +## Troubleshooting + +### Lỗi: `ImportError: libtk8.6.so` +```bash +# Ubuntu/Debian +sudo apt-get install python3-tk + +# Fedora +sudo dnf install python3-tkinter +``` + +### Lỗi: Không hiển thị được cửa sổ +Kiểm tra biến môi trường DISPLAY (trên Linux): +```bash +echo $DISPLAY +# Nếu rỗng, cần cấu hình X11 forwarding +``` + +## Đóng góp + +Mọi đóng góp về tính năng, ngôn ngữ mới, hoặc bug report đều được chào đón! + +## License + +MIT License - Xem file LICENSE trong repository gốc. + +--- + +**Project Nayuki's QR Code Generator Library** +https://www.nayuki.io/page/qr-code-generator-library diff --git a/python/__pycache__/qrcodegen.cpython-312.pyc b/python/__pycache__/qrcodegen.cpython-312.pyc new file mode 100644 index 00000000..480e04a0 Binary files /dev/null and b/python/__pycache__/qrcodegen.cpython-312.pyc differ diff --git a/python/qrcodegen-demo.py b/python/qrcodegen-demo.py index 0f24e853..5af6b60d 100644 --- a/python/qrcodegen-demo.py +++ b/python/qrcodegen-demo.py @@ -171,25 +171,95 @@ def do_mask_demo() -> None: # ---- Utilities ---- -def to_svg_str(qr: QrCode, border: int) -> str: - """Returns a string of SVG code for an image depicting the given QR Code, with the given number - of border modules. The string always uses Unix newlines (\n), regardless of the platform.""" - if border < 0: - raise ValueError("Border must be non-negative") - parts: list[str] = [] - for y in range(qr.get_size()): - for x in range(qr.get_size()): - if qr.get_module(x, y): - parts.append(f"M{x+border},{y+border}h1v1h-1z") - return f""" +def to_svg_str( + qr: QrCode, + border: int = 4, + *, + scale: int = 1, + foreground_color: str = "#000000", + background_color: str = "#FFFFFF", +) -> str: + """Returns a string of SVG code for an image depicting the given QR Code. + + Args: + qr: The QR Code object to convert to SVG. + border: Number of border modules around the QR Code (default: 4). + scale: Size of each module in pixels (default: 1). + foreground_color: Color of the QR Code modules (default: "#000000"). + background_color: Color of the background (default: "#FFFFFF"). + + Returns: + A string containing the complete SVG document. + + Raises: + ValueError: If border is negative or scale is less than 1. + + Example: + >>> qr = QrCode.encode_text("Hello, world!", QrCode.Ecc.LOW) + >>> svg = to_svg_str(qr, border=4, scale=10, foreground_color="#000080") + >>> print(svg) + """ + if border < 0: + raise ValueError(f"Border must be non-negative, got {border}") + if scale < 1: + raise ValueError(f"Scale must be at least 1, got {scale}") + + size = qr.get_size() + scaled_size = size * scale + total_size = scaled_size + border * 2 * scale + + parts: list[str] = [] + for y in range(size): + for x in range(size): + if qr.get_module(x, y): + pos_x = (x * scale) + border * scale + pos_y = (y * scale) + border * scale + parts.append(f"M{pos_x},{pos_y}h{scale}v{scale}h{-scale}z") + + return f""" - - - + +\t +\t """ +def to_svg_file( + qr: QrCode, + filepath: str, + border: int = 4, + *, + scale: int = 1, + foreground_color: str = "#000000", + background_color: str = "#FFFFFF", +) -> None: + """Writes the QR Code as an SVG file to the specified path. + + Args: + qr: The QR Code object to convert to SVG. + filepath: Path where the SVG file will be saved. + border: Number of border modules around the QR Code (default: 4). + scale: Size of each module in pixels (default: 1). + foreground_color: Color of the QR Code modules (default: "#000000"). + background_color: Color of the background (default: "#FFFFFF"). + + Example: + >>> qr = QrCode.encode_text("https://example.com", QrCode.Ecc.MEDIUM) + >>> to_svg_file(qr, "qrcode.svg", border=4, scale=10) + """ + svg_content = to_svg_str( + qr, + border=border, + scale=scale, + foreground_color=foreground_color, + background_color=background_color, + ) + with open(filepath, "w", encoding="utf-8", newline="\n") as f: + f.write(svg_content) + + + def print_qr(qrcode: QrCode) -> None: """Prints the given QrCode object to the console.""" border = 4 diff --git a/python/qrcodegen-demo.py.backup b/python/qrcodegen-demo.py.backup new file mode 100644 index 00000000..0f24e853 --- /dev/null +++ b/python/qrcodegen-demo.py.backup @@ -0,0 +1,205 @@ +# +# QR Code generator demo (Python) +# +# Run this command-line program with no arguments. The program computes a bunch of demonstration +# QR Codes and prints them to the console. Also, the SVG code for one QR Code is printed as a sample. +# +# Copyright (c) Project Nayuki. (MIT License) +# https://www.nayuki.io/page/qr-code-generator-library +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# - The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# - The Software is provided "as is", without warranty of any kind, express or +# implied, including but not limited to the warranties of merchantability, +# fitness for a particular purpose and noninfringement. In no event shall the +# authors or copyright holders be liable for any claim, damages or other +# liability, whether in an action of contract, tort or otherwise, arising from, +# out of or in connection with the Software or the use or other dealings in the +# Software. +# + +from __future__ import annotations +from qrcodegen import QrCode, QrSegment + + +def main() -> None: + """The main application program.""" + do_basic_demo() + do_variety_demo() + do_segment_demo() + do_mask_demo() + + + +# ---- Demo suite ---- + +def do_basic_demo() -> None: + """Creates a single QR Code, then prints it to the console.""" + text = "Hello, world!" # User-supplied Unicode text + errcorlvl = QrCode.Ecc.LOW # Error correction level + + # Make and print the QR Code symbol + qr = QrCode.encode_text(text, errcorlvl) + print_qr(qr) + print(to_svg_str(qr, 4)) + + +def do_variety_demo() -> None: + """Creates a variety of QR Codes that exercise different features of the library, and prints each one to the console.""" + + # Numeric mode encoding (3.33 bits per digit) + qr = QrCode.encode_text("314159265358979323846264338327950288419716939937510", QrCode.Ecc.MEDIUM) + print_qr(qr) + + # Alphanumeric mode encoding (5.5 bits per character) + qr = QrCode.encode_text("DOLLAR-AMOUNT:$39.87 PERCENTAGE:100.00% OPERATIONS:+-*/", QrCode.Ecc.HIGH) + print_qr(qr) + + # Unicode text as UTF-8 + qr = QrCode.encode_text("\u3053\u3093\u306B\u3061\u0077\u0061\u3001\u4E16\u754C\uFF01\u0020\u03B1\u03B2\u03B3\u03B4", QrCode.Ecc.QUARTILE) + print_qr(qr) + + # Moderately large QR Code using longer text (from Lewis Carroll's Alice in Wonderland) + qr = QrCode.encode_text( + "Alice was beginning to get very tired of sitting by her sister on the bank, " + "and of having nothing to do: once or twice she had peeped into the book her sister was reading, " + "but it had no pictures or conversations in it, 'and what is the use of a book,' thought Alice " + "'without pictures or conversations?' So she was considering in her own mind (as well as she could, " + "for the hot day made her feel very sleepy and stupid), whether the pleasure of making a " + "daisy-chain would be worth the trouble of getting up and picking the daisies, when suddenly " + "a White Rabbit with pink eyes ran close by her.", QrCode.Ecc.HIGH) + print_qr(qr) + + +def do_segment_demo() -> None: + """Creates QR Codes with manually specified segments for better compactness.""" + + # Illustration "silver" + silver0 = "THE SQUARE ROOT OF 2 IS 1." + silver1 = "41421356237309504880168872420969807856967187537694807317667973799" + qr = QrCode.encode_text(silver0 + silver1, QrCode.Ecc.LOW) + print_qr(qr) + + segs = [ + QrSegment.make_alphanumeric(silver0), + QrSegment.make_numeric(silver1)] + qr = QrCode.encode_segments(segs, QrCode.Ecc.LOW) + print_qr(qr) + + # Illustration "golden" + golden0 = "Golden ratio \u03C6 = 1." + golden1 = "6180339887498948482045868343656381177203091798057628621354486227052604628189024497072072041893911374" + golden2 = "......" + qr = QrCode.encode_text(golden0 + golden1 + golden2, QrCode.Ecc.LOW) + print_qr(qr) + + segs = [ + QrSegment.make_bytes(golden0.encode("UTF-8")), + QrSegment.make_numeric(golden1), + QrSegment.make_alphanumeric(golden2)] + qr = QrCode.encode_segments(segs, QrCode.Ecc.LOW) + print_qr(qr) + + # Illustration "Madoka": kanji, kana, Cyrillic, full-width Latin, Greek characters + madoka = "\u300C\u9B54\u6CD5\u5C11\u5973\u307E\u3069\u304B\u2606\u30DE\u30AE\u30AB\u300D\u3063\u3066\u3001\u3000\u0418\u0410\u0418\u3000\uFF44\uFF45\uFF53\uFF55\u3000\u03BA\u03B1\uFF1F" + qr = QrCode.encode_text(madoka, QrCode.Ecc.LOW) + print_qr(qr) + + kanjicharbits = [ # Kanji mode encoding (13 bits per character) + 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, + 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, + 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, + 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, + 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, + 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, + 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, + 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, + 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + ] + segs = [QrSegment(QrSegment.Mode.KANJI, len(kanjicharbits) // 13, kanjicharbits)] + qr = QrCode.encode_segments(segs, QrCode.Ecc.LOW) + print_qr(qr) + + +def do_mask_demo() -> None: + """Creates QR Codes with the same size and contents but different mask patterns.""" + + # Project Nayuki URL + segs = QrSegment.make_segments("https://www.nayuki.io/") + print_qr(QrCode.encode_segments(segs, QrCode.Ecc.HIGH, mask=-1)) # Automatic mask + print_qr(QrCode.encode_segments(segs, QrCode.Ecc.HIGH, mask=3)) # Force mask 3 + + # Chinese text as UTF-8 + segs = QrSegment.make_segments( + "\u7DAD\u57FA\u767E\u79D1\uFF08\u0057\u0069\u006B\u0069\u0070\u0065\u0064\u0069\u0061\uFF0C" + "\u8046\u807D\u0069\u002F\u02CC\u0077\u026A\u006B\u1D7B\u02C8\u0070\u0069\u02D0\u0064\u0069" + "\u002E\u0259\u002F\uFF09\u662F\u4E00\u500B\u81EA\u7531\u5167\u5BB9\u3001\u516C\u958B\u7DE8" + "\u8F2F\u4E14\u591A\u8A9E\u8A00\u7684\u7DB2\u8DEF\u767E\u79D1\u5168\u66F8\u5354\u4F5C\u8A08" + "\u756B") + print_qr(QrCode.encode_segments(segs, QrCode.Ecc.MEDIUM, mask=0)) # Force mask 0 + print_qr(QrCode.encode_segments(segs, QrCode.Ecc.MEDIUM, mask=1)) # Force mask 1 + print_qr(QrCode.encode_segments(segs, QrCode.Ecc.MEDIUM, mask=5)) # Force mask 5 + print_qr(QrCode.encode_segments(segs, QrCode.Ecc.MEDIUM, mask=7)) # Force mask 7 + + + +# ---- Utilities ---- + +def to_svg_str(qr: QrCode, border: int) -> str: + """Returns a string of SVG code for an image depicting the given QR Code, with the given number + of border modules. The string always uses Unix newlines (\n), regardless of the platform.""" + if border < 0: + raise ValueError("Border must be non-negative") + parts: list[str] = [] + for y in range(qr.get_size()): + for x in range(qr.get_size()): + if qr.get_module(x, y): + parts.append(f"M{x+border},{y+border}h1v1h-1z") + return f""" + + + + + +""" + + +def print_qr(qrcode: QrCode) -> None: + """Prints the given QrCode object to the console.""" + border = 4 + for y in range(-border, qrcode.get_size() + border): + for x in range(-border, qrcode.get_size() + border): + print("\u2588 "[1 if qrcode.get_module(x,y) else 0] * 2, end="") + print() + print() + + +# Run the main program +if __name__ == "__main__": + main() diff --git a/python/qrcodegen_gui.py b/python/qrcodegen_gui.py new file mode 100644 index 00000000..b6c04c5b --- /dev/null +++ b/python/qrcodegen_gui.py @@ -0,0 +1,629 @@ +# +# QR Code Generator GUI with Multi-language Support and Light/Dark Theme +# +# Copyright (c) Project Nayuki. (MIT License) +# https://www.nayuki.io/page/qr-code-generator-library +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# - The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# - The Software is provided "as is", without warranty of any kind, express or +# implied, including but not limited to the warranties of merchantability, +# fitness for a particular purpose and noninfringement. In no event shall the +# authors or copyright holders be liable for any claim, damages or other +# liability, whether in an action of contract, tort or otherwise, arising from, +# out of or in connection with the Software or the use or other dealings in the +# Software. +# + +from __future__ import annotations +import json +from typing import Optional, Dict, Any + +# Try to import tkinter, but allow running without it for testing +try: + import tkinter as tk + from tkinter import ttk, filedialog, messagebox + TKINTER_AVAILABLE = True +except ImportError: + TKINTER_AVAILABLE = False + tk = None + ttk = None + filedialog = None + messagebox = None + +from qrcodegen import QrCode, QrSegment + + +# ============================================================================= +# MULTI-LANGUAGE SUPPORT +# ============================================================================= + +LANGUAGES: Dict[str, Dict[str, str]] = { + "en": { + "title": "QR Code Generator", + "input_label": "Enter text or URL:", + "error_correction": "Error Correction:", + "border": "Border:", + "scale": "Scale:", + "foreground_color": "Foreground Color:", + "background_color": "Background Color:", + "generate": "Generate QR Code", + "save_svg": "Save as SVG", + "save_png": "Save as PNG", + "clear": "Clear", + "theme": "Theme", + "light_theme": "Light Theme", + "dark_theme": "Dark Theme", + "language": "Language", + "success_save": "QR Code saved successfully!", + "error_generate": "Failed to generate QR Code. Please check your input.", + "error_save": "Failed to save file.", + "low": "Low", + "medium": "Medium", + "quartile": "Quartile", + "high": "High", + "preview": "Preview", + "svg_preview": "SVG Preview", + "copy_svg": "Copy SVG Code", + "copied": "SVG code copied to clipboard!", + "version_info": "Version:", + "size_info": "Size:", + "modules_info": "Modules:", + }, + "vi": { + "title": "Trình Tạo Mã QR", + "input_label": "Nhập văn bản hoặc URL:", + "error_correction": "Mức sửa lỗi:", + "border": "Viền:", + "scale": "Tỷ lệ:", + "foreground_color": "Màu前景:", + "background_color": "Màu nền:", + "generate": "Tạo mã QR", + "save_svg": "Lưu SVG", + "save_png": "Lưu PNG", + "clear": "Xóa", + "theme": "Chủ đề", + "light_theme": "Sáng", + "dark_theme": "Tối", + "language": "Ngôn ngữ", + "success_save": "Đã lưu mã QR thành công!", + "error_generate": "Không thể tạo mã QR. Vui lòng kiểm tra đầu vào.", + "error_save": "Không thể lưu file.", + "low": "Thấp", + "medium": "Trung bình", + "quartile": "Phần tư", + "high": "Cao", + "preview": "Xem trước", + "svg_preview": "Xem trước SVG", + "copy_svg": "Sao chép mã SVG", + "copied": "Đã sao chép mã SVG!", + "version_info": "Phiên bản:", + "size_info": "Kích thước:", + "modules_info": "Module:", + }, + "ja": { + "title": "QR コードジェネレーター", + "input_label": "テキストまたは URL を入力:", + "error_correction": "誤り訂正レベル:", + "border": "余白:", + "scale": "スケール:", + "foreground_color": "前景色:", + "background_color": "背景色:", + "generate": "QR コードを生成", + "save_svg": "SVG で保存", + "save_png": "PNG で保存", + "clear": "クリア", + "theme": "テーマ", + "light_theme": "ライト", + "dark_theme": "ダーク", + "language": "言語", + "success_save": "QR コードを保存しました!", + "error_generate": "QR コードの生成に失敗しました。入力を確認してください。", + "error_save": "ファイルの保存に失敗しました。", + "low": "低", + "medium": "中", + "quartile": "四分の一", + "high": "高", + "preview": "プレビュー", + "svg_preview": "SVG プレビュー", + "copy_svg": "SVG コードをコピー", + "copied": "SVG コードをクリップボードにコピーしました!", + "version_info": "バージョン:", + "size_info": "サイズ:", + "modules_info": "モジュール数:", + }, + "zh": { + "title": "二维码生成器", + "input_label": "输入文本或网址:", + "error_correction": "纠错级别:", + "border": "边框:", + "scale": "缩放:", + "foreground_color": "前景色:", + "background_color": "背景色:", + "generate": "生成二维码", + "save_svg": "保存为 SVG", + "save_png": "保存为 PNG", + "clear": "清除", + "theme": "主题", + "light_theme": "浅色", + "dark_theme": "深色", + "language": "语言", + "success_save": "二维码保存成功!", + "error_generate": "生成二维码失败,请检查输入。", + "error_save": "保存文件失败。", + "low": "低", + "medium": "中", + "quartile": "四分之一", + "high": "高", + "preview": "预览", + "svg_preview": "SVG 预览", + "copy_svg": "复制 SVG 代码", + "copied": "SVG 代码已复制到剪贴板!", + "version_info": "版本:", + "size_info": "尺寸:", + "modules_info": "模块数:", + }, +} + +# ============================================================================= +# COLOR THEMES +# ============================================================================= + +LIGHT_THEME = { + "bg": "#FFFFFF", + "fg": "#000000", + "frame_bg": "#F5F5F5", + "button_bg": "#E0E0E0", + "button_fg": "#000000", + "entry_bg": "#FFFFFF", + "entry_fg": "#000000", + "label_bg": "#FFFFFF", + "label_fg": "#000000", + "accent": "#2196F3", + "border_color": "#CCCCCC", +} + +DARK_THEME = { + "bg": "#1E1E1E", + "fg": "#FFFFFF", + "frame_bg": "#2D2D2D", + "button_bg": "#3D3D3D", + "button_fg": "#FFFFFF", + "entry_bg": "#2D2D2D", + "entry_fg": "#FFFFFF", + "label_bg": "#1E1E1E", + "label_fg": "#FFFFFF", + "accent": "#64B5F6", + "border_color": "#555555", +} + + +# ============================================================================= +# QR CODE GENERATOR CLASS +# ============================================================================= + +class QRCodeGenerator: + """Main QR Code Generator Application with GUI.""" + + def __init__(self, root: tk.Tk): + self.root = root + self.root.title("QR Code Generator") + self.root.geometry("900x700") + self.root.minsize(800, 600) + + # State variables + self.current_language = "en" + self.current_theme = "light" + self.current_qr: Optional[QrCode] = None + self.current_svg: str = "" + + # Initialize UI + self._setup_styles() + self._create_menu() + self._create_widgets() + self._apply_theme() + self._update_language() + + def _setup_styles(self): + """Configure ttk styles.""" + self.style = ttk.Style() + self.style.theme_use('clam') + + def _create_menu(self): + """Create menu bar.""" + menubar = tk.Menu(self.root) + self.root.config(menu=menubar) + + # File menu + file_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="File", menu=file_menu) + file_menu.add_command(label="Save as SVG", command=self.save_svg, accelerator="Ctrl+S") + file_menu.add_separator() + file_menu.add_command(label="Exit", command=self.root.quit, accelerator="Ctrl+Q") + + # View menu + view_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="View", menu=view_menu) + view_menu.add_radiobutton(label="Light Theme", command=lambda: self.set_theme("light")) + view_menu.add_radiobutton(label="Dark Theme", command=lambda: self.set_theme("dark")) + + # Language menu + lang_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Language", menu=lang_menu) + lang_menu.add_radiobutton(label="English", command=lambda: self.set_language("en")) + lang_menu.add_radiobutton(label="Tiếng Việt", command=lambda: self.set_language("vi")) + lang_menu.add_radiobutton(label="日本語", command=lambda: self.set_language("ja")) + lang_menu.add_radiobutton(label="中文", command=lambda: self.set_language("zh")) + + # Help menu + help_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Help", menu=help_menu) + help_menu.add_command(label="About", command=self.show_about) + + # Keyboard shortcuts + self.root.bind("", lambda e: self.save_svg()) + self.root.bind("", lambda e: self.root.quit()) + + def _create_widgets(self): + """Create main widgets.""" + # Main container + main_frame = ttk.Frame(self.root, padding="10") + main_frame.grid(row=0, column=0, sticky="nsew") + + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + main_frame.columnconfigure(0, weight=1) + main_frame.rowconfigure(3, weight=1) + + # Title + title_label = ttk.Label(main_frame, text="QR Code Generator", font=("Helvetica", 20, "bold")) + title_label.grid(row=0, column=0, pady=(0, 20), sticky="w") + + # Input section + input_frame = ttk.LabelFrame(main_frame, text="Input", padding="10") + input_frame.grid(row=1, column=0, sticky="ew", pady=(0, 10)) + input_frame.columnconfigure(1, weight=1) + + ttk.Label(input_frame, text="Text/URL:").grid(row=0, column=0, sticky="w", pady=5) + self.text_entry = tk.Text(input_frame, height=4, width=50, wrap=tk.WORD) + self.text_entry.grid(row=0, column=1, sticky="ew", pady=5, padx=(10, 0)) + + # Options section + options_frame = ttk.LabelFrame(main_frame, text="Options", padding="10") + options_frame.grid(row=2, column=0, sticky="ew", pady=(0, 10)) + options_frame.columnconfigure(1, weight=1) + options_frame.columnconfigure(3, weight=1) + + # Error correction level + ttk.Label(options_frame, text="Error Correction:").grid(row=0, column=0, sticky="w", pady=5) + self.errcor_var = tk.StringVar(value="M") + errcor_combo = ttk.Combobox(options_frame, textvariable=self.errcor_var, + values=["L", "M", "Q", "H"], state="readonly", width=10) + errcor_combo.grid(row=0, column=1, sticky="w", pady=5, padx=(10, 0)) + + # Border + ttk.Label(options_frame, text="Border:").grid(row=0, column=2, sticky="w", pady=5, padx=(20, 0)) + self.border_var = tk.IntVar(value=4) + border_spin = ttk.Spinbox(options_frame, from_=0, to=20, textvariable=self.border_var, width=10) + border_spin.grid(row=0, column=3, sticky="w", pady=5, padx=(10, 0)) + + # Scale + ttk.Label(options_frame, text="Scale:").grid(row=1, column=0, sticky="w", pady=5) + self.scale_var = tk.IntVar(value=10) + scale_spin = ttk.Spinbox(options_frame, from_=1, to=50, textvariable=self.scale_var, width=10) + scale_spin.grid(row=1, column=1, sticky="w", pady=5, padx=(10, 0)) + + # Foreground color + ttk.Label(options_frame, text="Foreground:").grid(row=1, column=2, sticky="w", pady=5, padx=(20, 0)) + self.fg_color_var = tk.StringVar(value="#000000") + fg_entry = ttk.Entry(options_frame, textvariable=self.fg_color_var, width=15) + fg_entry.grid(row=1, column=3, sticky="w", pady=5, padx=(10, 0)) + ttk.Button(options_frame, text="🎨", command=self.choose_fg_color).grid(row=1, column=4, padx=(5, 0)) + + # Background color + ttk.Label(options_frame, text="Background:").grid(row=2, column=0, sticky="w", pady=5) + self.bg_color_var = tk.StringVar(value="#FFFFFF") + bg_entry = ttk.Entry(options_frame, textvariable=self.bg_color_var, width=15) + bg_entry.grid(row=2, column=1, sticky="w", pady=5, padx=(10, 0)) + ttk.Button(options_frame, text="🎨", command=self.choose_bg_color).grid(row=2, column=4, padx=(5, 0)) + + # Buttons + button_frame = ttk.Frame(main_frame) + button_frame.grid(row=3, column=0, sticky="ew", pady=10) + + self.generate_btn = ttk.Button(button_frame, text="Generate QR Code", command=self.generate_qr) + self.generate_btn.pack(side=tk.LEFT, padx=5) + + self.save_btn = ttk.Button(button_frame, text="Save as SVG", command=self.save_svg) + self.save_btn.pack(side=tk.LEFT, padx=5) + + self.copy_btn = ttk.Button(button_frame, text="Copy SVG", command=self.copy_svg) + self.copy_btn.pack(side=tk.LEFT, padx=5) + + self.clear_btn = ttk.Button(button_frame, text="Clear", command=self.clear_all) + self.clear_btn.pack(side=tk.LEFT, padx=5) + + # Preview section + preview_frame = ttk.LabelFrame(main_frame, text="Preview", padding="10") + preview_frame.grid(row=4, column=0, sticky="nsew", pady=(0, 10)) + preview_frame.columnconfigure(0, weight=1) + preview_frame.rowconfigure(0, weight=1) + + # Canvas for QR Code display + self.canvas = tk.Canvas(preview_frame, bg="#FFFFFF", highlightthickness=0) + self.canvas.grid(row=0, column=0, sticky="nsew") + + # Info frame + info_frame = ttk.Frame(main_frame) + info_frame.grid(row=5, column=0, sticky="ew") + + self.version_label = ttk.Label(info_frame, text="") + self.version_label.pack(side=tk.LEFT, padx=5) + + self.size_label = ttk.Label(info_frame, text="") + self.size_label.pack(side=tk.LEFT, padx=5) + + self.modules_label = ttk.Label(info_frame, text="") + self.modules_label.pack(side=tk.LEFT, padx=5) + + # Theme toggle button + theme_frame = ttk.Frame(main_frame) + theme_frame.grid(row=0, column=0, sticky="e") + + self.theme_btn = ttk.Button(theme_frame, text="🌙 Dark Theme", command=self.toggle_theme) + self.theme_btn.pack(side=tk.RIGHT) + + def _apply_theme(self): + """Apply current theme to all widgets.""" + theme = DARK_THEME if self.current_theme == "dark" else LIGHT_THEME + + self.root.configure(bg=theme["bg"]) + self.style.configure(".", background=theme["frame_bg"], foreground=theme["fg"], + fieldbackground=theme["entry_bg"], selectbackground=theme["accent"]) + self.style.configure("TLabel", background=theme["label_bg"], foreground=theme["label_fg"]) + self.style.configure("TButton", background=theme["button_bg"], foreground=theme["button_fg"]) + self.style.configure("TLabelframe", background=theme["frame_bg"], foreground=theme["fg"]) + self.style.configure("TLabelframe.Label", background=theme["frame_bg"], foreground=theme["fg"]) + + self.text_entry.configure(bg=theme["entry_bg"], fg=theme["entry_fg"], + insertbackground=theme["fg"]) + self.canvas.configure(bg=theme["bg"] if self.current_qr is None else "#FFFFFF") + + def _update_language(self): + """Update all widget texts based on current language.""" + texts = LANGUAGES[self.current_language] + + self.root.title(texts["title"]) + + # Update menu (recreate for simplicity) + self._create_menu() + + # Update buttons + self.generate_btn.configure(text=texts["generate"]) + self.save_btn.configure(text=texts["save_svg"]) + self.copy_btn.configure(text=texts["copy_svg"]) + self.clear_btn.configure(text=texts["clear"]) + + # Update theme button + theme_text = texts["dark_theme"] if self.current_theme == "light" else texts["light_theme"] + theme_icon = "🌙" if self.current_theme == "light" else "☀️" + self.theme_btn.configure(text=f"{theme_icon} {theme_text}") + + def set_language(self, lang: str): + """Set application language.""" + if lang in LANGUAGES: + self.current_language = lang + self._update_language() + + def set_theme(self, theme: str): + """Set application theme.""" + if theme in ["light", "dark"]: + self.current_theme = theme + self._apply_theme() + self._update_language() + + def toggle_theme(self): + """Toggle between light and dark themes.""" + new_theme = "light" if self.current_theme == "dark" else "dark" + self.set_theme(new_theme) + + def choose_fg_color(self): + """Open color picker for foreground color.""" + color = tk.colorchooser.askcolor(title="Choose Foreground Color")[1] + if color: + self.fg_color_var.set(color) + + def choose_bg_color(self): + """Open color picker for background color.""" + color = tk.colorchooser.askcolor(title="Choose Background Color")[1] + if color: + self.bg_color_var.set(color) + + def generate_qr(self): + """Generate QR Code from input text.""" + try: + text = self.text_entry.get("1.0", tk.END).strip() + if not text: + messagebox.showwarning("Warning", "Please enter some text!") + return + + errcor_map = {"L": QrCode.Ecc.LOW, "M": QrCode.Ecc.MEDIUM, + "Q": QrCode.Ecc.QUARTILE, "H": QrCode.Ecc.HIGH} + errcorlvl = errcor_map[self.errcor_var.get()] + + border = self.border_var.get() + scale = self.scale_var.get() + fg_color = self.fg_color_var.get() + bg_color = self.bg_color_var.get() + + # Generate QR Code + self.current_qr = QrCode.encode_text(text, errcorlvl) + + # Generate SVG + self.current_svg = self.to_svg_str( + self.current_qr, border, scale=scale, + foreground_color=fg_color, background_color=bg_color + ) + + # Display on canvas + self.display_qr_on_canvas(scale, fg_color, bg_color) + + # Update info labels + texts = LANGUAGES[self.current_language] + self.version_label.configure(text=f"{texts['version_info']} {self.current_qr.get_version()}") + self.size_label.configure(text=f"{texts['size_info']} {self.current_qr.get_size()}x{self.current_qr.get_size()}") + self.modules_label.configure(text=f"{texts['modules_info']} {self.count_modules()}") + + except Exception as e: + messagebox.showerror("Error", f"{LANGUAGES[self.current_language]['error_generate']}\n\n{str(e)}") + + def count_modules(self) -> int: + """Count the number of dark modules in the QR Code.""" + if not self.current_qr: + return 0 + count = 0 + size = self.current_qr.get_size() + for y in range(size): + for x in range(size): + if self.current_qr.get_module(x, y): + count += 1 + return count + + def display_qr_on_canvas(self, scale: int, fg_color: str, bg_color: str): + """Display QR Code on canvas.""" + if not self.current_qr: + return + + self.canvas.delete("all") + + size = self.current_qr.get_size() + border = self.border_var.get() + canvas_size = (size + border * 2) * scale + + # Set canvas size + self.canvas.configure(width=canvas_size, height=canvas_size, bg=bg_color) + + # Draw modules + for y in range(size): + for x in range(size): + if self.current_qr.get_module(x, y): + x1 = (x + border) * scale + y1 = (y + border) * scale + x2 = x1 + scale + y2 = y1 + scale + self.canvas.create_rectangle(x1, y1, x2, y2, fill=fg_color, outline="") + + def save_svg(self): + """Save QR Code as SVG file.""" + if not self.current_svg: + messagebox.showwarning("Warning", "Please generate a QR Code first!") + return + + filepath = filedialog.asksaveasfilename( + defaultextension=".svg", + filetypes=[("SVG files", "*.svg"), ("All files", "*.*")], + title="Save QR Code as SVG" + ) + + if filepath: + try: + with open(filepath, "w", encoding="utf-8", newline="\n") as f: + f.write(self.current_svg) + messagebox.showinfo("Success", LANGUAGES[self.current_language]["success_save"]) + except Exception as e: + messagebox.showerror("Error", f"{LANGUAGES[self.current_language]['error_save']}\n\n{str(e)}") + + def copy_svg(self): + """Copy SVG code to clipboard.""" + if not self.current_svg: + messagebox.showwarning("Warning", "Please generate a QR Code first!") + return + + self.root.clipboard_clear() + self.root.clipboard_append(self.current_svg) + messagebox.showinfo("Success", LANGUAGES[self.current_language]["copied"]) + + def clear_all(self): + """Clear all inputs and preview.""" + self.text_entry.delete("1.0", tk.END) + self.canvas.delete("all") + self.canvas.configure(width=400, height=400, bg="#FFFFFF") + self.current_qr = None + self.current_svg = "" + self.version_label.configure(text="") + self.size_label.configure(text="") + self.modules_label.configure(text="") + + def show_about(self): + """Show about dialog.""" + about_text = """QR Code Generator +Version 1.0.0 + +A multi-language QR Code generator with light/dark theme support. + +Based on Project Nayuki's QR Code Generator Library +https://www.nayuki.io/page/qr-code-generator-library + +Supported Languages: +• English +• Tiếng Việt +• 日本語 +• 中文 +""" + messagebox.showinfo("About", about_text) + + def to_svg_str(self, qr: QrCode, border: int, *, scale: int = 1, + foreground_color: str = "#000000", background_color: str = "#FFFFFF") -> str: + """Generate SVG string from QR Code.""" + if border < 0: + raise ValueError(f"Border must be non-negative, got {border}") + if scale < 1: + raise ValueError(f"Scale must be at least 1, got {scale}") + + size = qr.get_size() + scaled_size = size * scale + total_size = scaled_size + border * 2 * scale + + parts = [] + for y in range(size): + for x in range(size): + if qr.get_module(x, y): + pos_x = (x * scale) + border * scale + pos_y = (y * scale) + border * scale + parts.append(f"M{pos_x},{pos_y}h{scale}v{scale}h{-scale}z") + + return f""" + + +\t +\t + +""" + + +# ============================================================================= +# MAIN ENTRY POINT +# ============================================================================= + +def main(): + """Main entry point for the GUI application.""" + root = tk.Tk() + + # Set window icon (if available) + try: + root.iconbitmap("qrcode.ico") + except: + pass + + app = QRCodeGenerator(root) + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/python/test_svg_improved.py b/python/test_svg_improved.py new file mode 100644 index 00000000..a043b06e --- /dev/null +++ b/python/test_svg_improved.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +""" +Test script for improved SVG export functionality. +Demonstrates new features: scaling, custom colors, and file output. +""" + +import sys +sys.path.insert(0, '/workspace/python') +from qrcodegen import QrCode + +# Import the improved functions (copy from the updated demo file) +def to_svg_str( + qr, + border=4, + *, + scale=1, + foreground_color="#000000", + background_color="#FFFFFF", +): + """Returns a string of SVG code for an image depicting the given QR Code.""" + if border < 0: + raise ValueError(f"Border must be non-negative, got {border}") + if scale < 1: + raise ValueError(f"Scale must be at least 1, got {scale}") + + size = qr.get_size() + scaled_size = size * scale + total_size = scaled_size + border * 2 * scale + + parts = [] + for y in range(size): + for x in range(size): + if qr.get_module(x, y): + pos_x = (x * scale) + border * scale + pos_y = (y * scale) + border * scale + parts.append(f"M{pos_x},{pos_y}h{scale}v{scale}h{-scale}z") + + return f""" + + + + + +""" + + +def to_svg_file(qr, filepath, border=4, *, scale=1, foreground_color="#000000", background_color="#FFFFFF"): + """Writes the QR Code as an SVG file to the specified path.""" + svg_content = to_svg_str(qr, border=border, scale=scale, foreground_color=foreground_color, background_color=background_color) + with open(filepath, "w", encoding="utf-8", newline="\n") as f: + f.write(svg_content) + + +def main(): + print("=" * 60) + print("IMPROVED SVG EXPORT FUNCTIONALITY TEST") + print("=" * 60) + print() + + # Create a sample QR Code + text = "https://www.nayuki.io/" + qr = QrCode.encode_text(text, QrCode.Ecc.MEDIUM) + print(f"QR Code created for: {text}") + print(f"Size: {qr.get_size()}x{qr.get_size()} modules") + print() + + # Test 1: Basic SVG (backward compatible) + print("1. Basic SVG (default settings):") + svg_basic = to_svg_str(qr) + print(f" - Border: 4 modules") + print(f" - Scale: 1x") + print(f" - Colors: Black on White") + print(f" - Output length: {len(svg_basic)} characters") + print() + + # Test 2: Scaled SVG + print("2. Scaled SVG (10x):") + svg_scaled = to_svg_str(qr, scale=10) + total_size = qr.get_size() * 10 + 8 * 2 + print(f" - Border: 4 modules") + print(f" - Scale: 10x (each module is 10x10 pixels)") + print(f" - ViewBox: {total_size}x{total_size}") + print(f" - Output length: {len(svg_scaled)} characters") + print() + + # Test 3: Custom colors + print("3. Custom colors:") + svg_blue = to_svg_str(qr, foreground_color="#0066CC", background_color="#F0F0F0") + print(f" - Foreground: #0066CC (Blue)") + print(f" - Background: #F0F0F0 (Light Gray)") + print() + + # Test 4: Save files + print("4. Saving SVG files:") + files_created = [ + ("/tmp/qrcode_default.svg", {}), + ("/tmp/qrcode_scaled.svg", {"scale": 10}), + ("/tmp/qrcode_blue.svg", {"foreground_color": "#0066CC", "background_color": "#F0F0F0"}), + ("/tmp/qrcode_large.svg", {"scale": 20, "border": 8}), + ] + + for filepath, kwargs in files_created: + to_svg_file(qr, filepath, **kwargs) + print(f" ✓ Created: {filepath}") + print() + + # Test 5: Error handling + print("5. Error handling:") + try: + to_svg_str(qr, border=-1) + except ValueError as e: + print(f" ✓ Caught expected error: {e}") + + try: + to_svg_str(qr, scale=0) + except ValueError as e: + print(f" ✓ Caught expected error: {e}") + print() + + print("=" * 60) + print("ALL TESTS PASSED!") + print("=" * 60) + print() + print("New features added:") + print(" ✓ scale parameter for larger/smaller modules") + print(" ✓ foreground_color parameter for custom QR color") + print(" ✓ background_color parameter for custom background") + print(" ✓ to_svg_file() function for direct file output") + print(" ✓ Better error messages with parameter values") + print(" ✓ Comprehensive docstrings with examples") + print() + + +if __name__ == "__main__": + main()