1.完成大部分功能(圖片下載功能尚未加上)
1
.gitignore
vendored
@ -3,3 +3,4 @@ uploads/*
|
|||||||
generated/*
|
generated/*
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
|
/generated_images
|
||||||
|
|||||||
46
CustomCircleItem.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import sys
|
||||||
|
from PySide6.QtWidgets import QGraphicsItem
|
||||||
|
from PySide6.QtGui import QPainter, QBrush, QPen
|
||||||
|
from PySide6.QtCore import QRectF, Qt
|
||||||
|
|
||||||
|
# 1. 建立一個自訂的 QGraphicsItem 類別來繪製圓形/橢圓
|
||||||
|
class CustomCircleItem(QGraphicsItem):
|
||||||
|
def __init__(self, x, y, width, height, color=Qt.red, is_circle=False, parent :QGraphicsItem =None):
|
||||||
|
super().__init__(parent = parent) # 呼叫父類別的建構子
|
||||||
|
# QRectF 定義了橢圓或圓形的邊界框
|
||||||
|
self.rect = parent.boundingRect()
|
||||||
|
self.rect.setX(self.rect.x() + x)
|
||||||
|
self.rect.setY(self.rect.y() + y)
|
||||||
|
|
||||||
|
self.color = color
|
||||||
|
self.is_circle = is_circle # 用於判斷是否繪製正圓
|
||||||
|
|
||||||
|
# 為了簡化,如果 is_circle 為 True,強制寬高相等
|
||||||
|
if is_circle:
|
||||||
|
side = max(width, height) # 取較大邊長作為圓的直徑
|
||||||
|
self.rect.setWidth(side)
|
||||||
|
self.rect.setHeight(side)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 必須實作此方法:定義項目的邊界矩形
|
||||||
|
def boundingRect(self):
|
||||||
|
# 返回定義橢圓或圓形所需空間的矩形
|
||||||
|
# 這對於繪圖更新、碰撞偵測和選擇非常重要
|
||||||
|
return self.rect
|
||||||
|
|
||||||
|
# 必須實作此方法:定義如何繪製項目
|
||||||
|
def paint(self, painter: QPainter, option, widget=None):
|
||||||
|
# 設定畫筆 (邊框)
|
||||||
|
pen = QPen(Qt.white) # 使用白色邊框
|
||||||
|
pen.setWidth(0)
|
||||||
|
painter.setPen(pen)
|
||||||
|
|
||||||
|
# 設定畫刷 (填充顏色)
|
||||||
|
brush = QBrush(self.color)
|
||||||
|
painter.setBrush(brush)
|
||||||
|
|
||||||
|
# 繪製橢圓或圓形
|
||||||
|
# drawEllipse(矩形): 在給定的矩形內繪製一個橢圓。
|
||||||
|
# 如果矩形的寬高相等,則繪製一個正圓。
|
||||||
|
painter.drawEllipse(self.rect)
|
||||||
18
app.py
@ -2,6 +2,7 @@
|
|||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from flask import Flask, render_template, request, send_file
|
from flask import Flask, render_template, request, send_file
|
||||||
|
import subprocess
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
@ -37,9 +38,26 @@ def upload_csv():
|
|||||||
if file.filename == '':
|
if file.filename == '':
|
||||||
return "No selected file", 400
|
return "No selected file", 400
|
||||||
if file and allowed_file(file.filename):
|
if file and allowed_file(file.filename):
|
||||||
|
|
||||||
unique_filename = str(uuid.uuid4()) + '.csv'
|
unique_filename = str(uuid.uuid4()) + '.csv'
|
||||||
csv_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
|
csv_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
|
||||||
file.save(csv_path)
|
file.save(csv_path)
|
||||||
|
print(f"CSV file saved to {csv_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 呼叫外部 Python 腳本來生成圖片
|
||||||
|
subprocess.run([os.path.join(os.getcwd(), '.venv', 'Scripts', 'python'), 'generate_images.py', csv_path], check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Error generating images: {e}")
|
||||||
|
return "Error generating images", 500
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
print(f"File not found: {e}")
|
||||||
|
return "File not found", 404
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An unexpected error occurred: {e}")
|
||||||
|
return "An unexpected error occurred", 500
|
||||||
|
|
||||||
|
|
||||||
return "File uploaded successfully", 200
|
return "File uploaded successfully", 200
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1,194 @@
|
|||||||
import sys
|
import sys
|
||||||
from PySide6.QtCore import QCoreApplication, QTimer, QObject, Signal, Slot
|
import csv
|
||||||
|
from PySide6.QtCore import Qt, QTimer, QObject, Signal, Slot
|
||||||
|
from PySide6.QtWidgets import QGraphicsScene, QGraphicsRectItem, QGraphicsPixmapItem,QApplication, QGraphicsTextItem
|
||||||
|
from PySide6.QtGui import QPainter, QImage, QColor, QPixmap, QFont, QFontDatabase
|
||||||
|
import CustomCircleItem
|
||||||
|
import re
|
||||||
|
|
||||||
|
BG_WIDTH = 3508
|
||||||
|
BG_HEIGHT = 2480
|
||||||
|
HEAD_ICON_PREFIX = "resource/head_%d.png"
|
||||||
|
|
||||||
|
X_OFFSET = 1700
|
||||||
|
Y_OFFSET = 650
|
||||||
|
|
||||||
|
CIRCLE_RADIUS = 617
|
||||||
|
CIRCLE_X_OFFSET = 750
|
||||||
|
|
||||||
|
def is_chinese(text):
|
||||||
|
# Returns True if any character is Chinese
|
||||||
|
return any('\u4e00' <= char <= '\u9fff' for char in text)
|
||||||
|
|
||||||
|
def is_english(text):
|
||||||
|
# Returns True if all characters are English letters (ignores spaces)
|
||||||
|
return bool(re.fullmatch(r'[A-Za-z\s]+', text))
|
||||||
|
|
||||||
|
class ImageGenerator(QObject):
|
||||||
|
finished = Signal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.scene = QGraphicsScene()
|
||||||
|
self._timer = QTimer(self)
|
||||||
|
self._timer.timeout.connect(self.generate_images)
|
||||||
|
|
||||||
|
self.colors = self.read_csv("resource/colors.csv")
|
||||||
|
|
||||||
|
print(f"Loaded colors: {self.colors}")
|
||||||
|
# Add the font file (provide the correct path)
|
||||||
|
self.customFont = self.read_font("resource/TaiwanPearl-SemiBold.ttf")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def read_csv(self, path):
|
||||||
|
colors = []
|
||||||
|
try:
|
||||||
|
with open(path, newline='', encoding='utf-8') as csvfile:
|
||||||
|
reader = csv.reader(csvfile)
|
||||||
|
for row in reader:
|
||||||
|
colors.append(row)
|
||||||
|
if colors:
|
||||||
|
colors.pop(0) # Remove header row if exists
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading {path}: {e}")
|
||||||
|
return colors
|
||||||
|
|
||||||
|
def read_font(self,path):
|
||||||
|
try:
|
||||||
|
font_id = QFontDatabase.addApplicationFont(path)
|
||||||
|
if font_id != -1:
|
||||||
|
families = QFontDatabase.applicationFontFamilies(font_id)
|
||||||
|
print("Loaded font families:", families)
|
||||||
|
family = families[1] if len(families) > 1 else families[0]
|
||||||
|
customFont = QFont(family)
|
||||||
|
customFont.setPixelSize(80)
|
||||||
|
customFont.setLetterSpacing(QFont.AbsoluteSpacing,15) # Adjust word spacing if needed
|
||||||
|
return customFont
|
||||||
|
else:
|
||||||
|
print("Failed to load font.")
|
||||||
|
return QFont() # fallback to default font
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading font from {path}: {e}")
|
||||||
|
return QFont()
|
||||||
|
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def start(self):
|
||||||
|
self._timer.start(1000) # Generate images every second
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def stop(self):
|
||||||
|
self._timer.stop()
|
||||||
|
self.finished.emit()
|
||||||
|
|
||||||
|
|
||||||
|
def scene_to_image(self, scene, filename):
|
||||||
|
print(f"Saving scene to {filename}...")
|
||||||
|
image = QImage(scene.sceneRect().size().toSize(), QImage.Format_ARGB32)
|
||||||
|
image.fill(Qt.transparent)
|
||||||
|
|
||||||
|
# Set 300 DPI
|
||||||
|
dpi = 300
|
||||||
|
dots_per_meter = int(dpi / 0.0254)
|
||||||
|
image.setDotsPerMeterX(dots_per_meter)
|
||||||
|
image.setDotsPerMeterY(dots_per_meter)
|
||||||
|
|
||||||
|
painter = QPainter(image)
|
||||||
|
scene.render(painter)
|
||||||
|
painter.end()
|
||||||
|
image.save(f"generated_images/{filename}", "PNG")
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def generate_images(self, name_csv):
|
||||||
|
# Placeholder for image generation logic
|
||||||
|
print("Generating images...1")
|
||||||
|
|
||||||
|
|
||||||
|
# Group of rect
|
||||||
|
for i, name in enumerate(name_csv):
|
||||||
|
r =g =b = 0
|
||||||
|
for j, color in enumerate(self.colors):
|
||||||
|
if color[0] == name[0]:
|
||||||
|
print(f"Adding rect with color: {color}")
|
||||||
|
r, g, b = map(int, color[1:4])
|
||||||
|
break
|
||||||
|
_idx_x = i % 2
|
||||||
|
_idx_y = (i%6) // 2
|
||||||
|
|
||||||
|
if i % 6 == 0:
|
||||||
|
# Example of adding a simple item to the scene
|
||||||
|
bg_item = QGraphicsRectItem(0, 0, BG_WIDTH, BG_HEIGHT)
|
||||||
|
bg_item.setBrush(Qt.white)
|
||||||
|
self.scene.addItem(bg_item)
|
||||||
|
print("Generating images...2")
|
||||||
|
|
||||||
|
rect_item = QGraphicsRectItem(30+X_OFFSET*_idx_x, 30+(Y_OFFSET+20)*_idx_y, X_OFFSET, Y_OFFSET, parent= bg_item)
|
||||||
|
rect_item.setPen(Qt.NoPen)
|
||||||
|
#simple pixmap
|
||||||
|
# smaple_item = QGraphicsPixmapItem(QPixmap("resource/sample.jpg"), rect_item)
|
||||||
|
# smaple_item.setPos(rect_item.boundingRect().x()+100, rect_item.boundingRect().y()+25)
|
||||||
|
|
||||||
|
#draw circle
|
||||||
|
for j in range(2):
|
||||||
|
circle_item = CustomCircleItem.CustomCircleItem(100+(CIRCLE_X_OFFSET*j), 25, CIRCLE_RADIUS, 600, QColor(r, g, b), is_circle=True,parent=rect_item)
|
||||||
|
circle_item.setOpacity(1)
|
||||||
|
|
||||||
|
#draw head icon
|
||||||
|
head_icon_num = int(name[1]) if len(name) > 1 else 1
|
||||||
|
head_icon = QGraphicsPixmapItem(QPixmap(HEAD_ICON_PREFIX % head_icon_num), circle_item)
|
||||||
|
head_icon.setPos(circle_item.boundingRect().x(), circle_item.boundingRect().y())
|
||||||
|
|
||||||
|
#name item
|
||||||
|
name_item = QGraphicsTextItem()
|
||||||
|
|
||||||
|
name_str = name[2 + j] if len(name) > 2 + j else "Unknown"
|
||||||
|
if is_chinese(name_str):
|
||||||
|
if len(name_str) == 2:
|
||||||
|
self.customFont.setLetterSpacing(QFont.AbsoluteSpacing, 30) # Adjust word spacing if needed
|
||||||
|
elif len(name_str) == 3:
|
||||||
|
self.customFont.setLetterSpacing(QFont.AbsoluteSpacing, 15)
|
||||||
|
elif len(name_str) == 4:
|
||||||
|
self.customFont.setLetterSpacing(QFont.AbsoluteSpacing, 0)
|
||||||
|
else:
|
||||||
|
self.customFont.setLetterSpacing(QFont.AbsoluteSpacing, 0)
|
||||||
|
name_item.setFont(self.customFont)
|
||||||
|
name_item.setPlainText(name_str)
|
||||||
|
name_item.setDefaultTextColor(QColor(0, 0, 0))
|
||||||
|
name_item.setPos((circle_item.boundingRect().x()+ circle_item.boundingRect().width()/2 - name_item.boundingRect().width()/2)+10, circle_item.boundingRect().y()+430)
|
||||||
|
self.scene.addItem(name_item)
|
||||||
|
|
||||||
|
if i > 0 and i % 6 == 5:
|
||||||
|
# Save the current scene to an image file every 5 items
|
||||||
|
self.scene_to_image(self.scene, f"generated_image_{i//6}.png")
|
||||||
|
self.scene.clear()
|
||||||
|
|
||||||
|
print("Image generated and added to scene.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
generator = ImageGenerator()
|
||||||
|
generator.finished.connect(app.quit)
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
csv_file = sys.argv[1]
|
||||||
|
print(f"Loading CSV file: {csv_file}")
|
||||||
|
else:
|
||||||
|
csv_file = "resource/sample.csv"
|
||||||
|
print(f"No CSV file provided, using default: {csv_file}")
|
||||||
|
|
||||||
|
sample_data = generator.read_csv(csv_file)
|
||||||
|
print(f"Sample data loaded: {sample_data}")
|
||||||
|
|
||||||
|
generator.generate_images(sample_data)
|
||||||
|
#generator.start()
|
||||||
|
|
||||||
|
# Run the application event loop
|
||||||
|
sys.exit(0)
|
||||||
BIN
resource/TaiwanPearl-SemiBold.ttf
Normal file
25
resource/colors.csv
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
color_name,r,g,b
|
||||||
|
海天藍,74,139,204
|
||||||
|
桃花紅,213,104,111
|
||||||
|
菸灰綠,181,188,177
|
||||||
|
奶油色,242,232,212
|
||||||
|
湖水藍,162,199,206
|
||||||
|
奶茶色,226,197,159
|
||||||
|
亮黃色,246,220,85
|
||||||
|
小松黃,199,154,98
|
||||||
|
天空藍,167,204,227
|
||||||
|
甜蜜粉,227,172,189
|
||||||
|
嫩綠色,172,207,120
|
||||||
|
燕麥灰,206,199,187
|
||||||
|
焦糖橙,216,165,138
|
||||||
|
蘋果綠,147,176,90
|
||||||
|
復古綠,125,148,136
|
||||||
|
水綠色,219,226,212
|
||||||
|
草綠色,114,162,75
|
||||||
|
紫藤色,142,126,168
|
||||||
|
紫灰色,179,171,186
|
||||||
|
淺藍灰,149,172,195
|
||||||
|
裸粉色,212,179,191
|
||||||
|
法藍色,132,173,175
|
||||||
|
薰衣紫,172,162,202
|
||||||
|
淡黃色,232,225,170
|
||||||
|
BIN
resource/head_1.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
resource/head_2.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
resource/head_3.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
resource/head_4.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
resource/head_5.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
resource/head_6.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
25
resource/sample.csv
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
color_name,icon_type,name_1,name_2
|
||||||
|
海天藍,1,陳宣瑜,Shirley
|
||||||
|
桃花紅,1,王子,陳匹股
|
||||||
|
菸灰綠,1,公主,公主
|
||||||
|
奶油色,1,吳佳鈴,吳佳鈴
|
||||||
|
湖水藍,5,陳守志,陳守志
|
||||||
|
奶茶色,6,陳宣瑜,陳宣瑜
|
||||||
|
亮黃色,1,王子,王子
|
||||||
|
小松黃,2,公主,公主
|
||||||
|
天空藍,3,吳佳鈴,吳佳鈴
|
||||||
|
甜蜜粉,4,陳守志,陳守志
|
||||||
|
嫩綠色,5,陳宣瑜,陳宣瑜
|
||||||
|
燕麥灰,6,王子,王子
|
||||||
|
焦糖橙,1,公主,公主
|
||||||
|
蘋果綠,2,吳佳鈴,吳佳鈴
|
||||||
|
復古綠,3,陳守志,陳守志
|
||||||
|
水綠色,4,陳宣瑜,陳宣瑜
|
||||||
|
草綠色,5,王子,王子
|
||||||
|
紫藤色,6,公主,公主
|
||||||
|
紫灰色,1,吳佳鈴,吳佳鈴
|
||||||
|
淺藍灰,2,陳守志,陳守志
|
||||||
|
裸粉色,3,陳宣瑜,陳宣瑜
|
||||||
|
法藍色,4,王子,王子
|
||||||
|
薰衣紫,5,公主,公主
|
||||||
|
淡黃色,6,吳佳鈴,吳佳鈴
|
||||||
|
BIN
resource/sample.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
resource/來圖圓形鑰匙圈-賣場-02.jpg
Normal file
|
After Width: | Height: | Size: 784 KiB |