Skip to content

Commit e218eb7

Browse files
Refactor certificate generation in CertifiedBuilder class to improve error handling and logging. Introduce a temporary directory for certificate storage, optimize image processing, and enhance methods for downloading images and creating text images. Ensure robust exception handling throughout the certificate creation process.
1 parent 96433f6 commit e218eb7

1 file changed

Lines changed: 167 additions & 112 deletions

File tree

certified_builder/certified_builder.py

Lines changed: 167 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -3,140 +3,195 @@
33
from models.participant import Participant
44
from PIL import Image, ImageDraw, ImageFont
55
from io import BytesIO
6+
import os
67
from certified_builder.utils.fetch_file_certificate import fetch_file_certificate
7-
import textwrap
8+
import tempfile
89

9-
FONT_NAME="certified_builder/fonts/PinyonScript/PinyonScript-Regular.ttf"
10-
VALIDATION_CODE="certified_builder/fonts/ChakraPetch/ChakraPetch-SemiBold.ttf"
11-
DETAILS_FONT="certified_builder/fonts/ChakraPetch/ChakraPetch-Regular.ttf"
12-
TEXT_COLOR = (0, 0, 0) # Define black color as constant
10+
FONT_NAME = os.path.join(os.path.dirname(__file__), "fonts/PinyonScript/PinyonScript-Regular.ttf")
11+
VALIDATION_CODE = os.path.join(os.path.dirname(__file__), "fonts/ChakraPetch/ChakraPetch-SemiBold.ttf")
12+
DETAILS_FONT = os.path.join(os.path.dirname(__file__), "fonts/ChakraPetch/ChakraPetch-Regular.ttf")
13+
TEXT_COLOR = (0, 0, 0)
1314

1415
logger = logging.getLogger(__name__)
1516

1617
class CertifiedBuilder:
18+
def __init__(self):
19+
# Ensure temp directory exists
20+
self.temp_dir = "/tmp/certificates"
21+
os.makedirs(self.temp_dir, exist_ok=True)
1722

1823
def build_certificates(self, participants: List[Participant]):
19-
logger.info(f"Iniciando geração de {len(participants)} certificados")
20-
for participant in participants:
21-
certificate_template = fetch_file_certificate(participant.certificate.background)
22-
logo = fetch_file_certificate(participant.certificate.logo)
23-
certificate_generated = self.generate_certificate(participant, certificate_template.copy(), logo)
24-
self.save_certificate(certificate_generated, participant)
25-
logger.info(f"Certificado gerado para {participant.name_completed()} com codigo de validação {participant.formated_validation_code()}")
24+
"""Build certificates for all participants."""
25+
try:
26+
logger.info(f"Iniciando geração de {len(participants)} certificados")
27+
results = []
28+
29+
for participant in participants:
30+
try:
31+
# Download template and logo with error handling
32+
certificate_template = self._download_image(participant.certificate.background)
33+
logo = self._download_image(participant.certificate.logo)
34+
35+
# Generate and save certificate
36+
certificate_generated = self.generate_certificate(participant, certificate_template, logo)
37+
certificate_path = self.save_certificate(certificate_generated, participant)
38+
39+
results.append({
40+
"participant": participant.dict(),
41+
"certificate_path": certificate_path,
42+
"success": True
43+
})
44+
45+
logger.info(f"Certificado gerado para {participant.name_completed()} com codigo de validação {participant.formated_validation_code()}")
46+
except Exception as e:
47+
logger.error(f"Erro ao gerar certificado para {participant.name_completed()}: {str(e)}")
48+
results.append({
49+
"participant": participant.dict(),
50+
"error": str(e),
51+
"success": False
52+
})
53+
54+
return results
55+
except Exception as e:
56+
logger.error(f"Erro geral na geração de certificados: {str(e)}")
57+
raise
58+
59+
def _download_image(self, url: str) -> Image:
60+
"""Download and open image with error handling."""
61+
try:
62+
return fetch_file_certificate(url)
63+
except Exception as e:
64+
logger.error(f"Erro ao baixar imagem de {url}: {str(e)}")
65+
raise RuntimeError(f"Error downloading image from {url}: {str(e)}")
2666

2767
def generate_certificate(self, participant: Participant, certificate_template: Image, logo: Image):
28-
# Create transparent layer for text and logo
29-
overlay = Image.new("RGBA", certificate_template.size, (255, 255, 255, 0))
30-
31-
# Resize logo to fit in top-left corner (e.g. 150x150 pixels)
32-
logo_size = (150, 150)
33-
logo = logo.resize(logo_size, Image.Resampling.LANCZOS)
34-
35-
# Paste logo in top-left corner with padding
36-
padding = 50
37-
overlay.paste(logo, (padding, padding), logo)
38-
39-
# Add name
40-
name_image = self.create_name_image(participant.name_completed(), certificate_template.size)
41-
overlay.paste(name_image, (0, 0), name_image)
42-
43-
# Add details text below name
44-
details_image = self.create_details_image(participant.certificate.details, certificate_template.size)
45-
# Calculate position for details (100 pixels below the name)
46-
name_center_y = certificate_template.size[1] // 2 # Center of the image
47-
details_y = name_center_y + 50 # 50 pixels below the center
48-
49-
# Create a new image for details with correct position
50-
details_with_position = Image.new("RGBA", certificate_template.size, (255, 255, 255, 0))
51-
details_with_position.paste(details_image, (0, details_y), details_image)
52-
53-
# Merge details with overlay
54-
overlay = Image.alpha_composite(overlay, details_with_position)
55-
56-
# Add validation code
57-
validation_code_image = self.create_validation_code_image(participant.formated_validation_code(), certificate_template.size)
58-
overlay.paste(validation_code_image, (0, 0), validation_code_image)
59-
60-
# Merge overlay with template
61-
result = Image.new("RGBA", certificate_template.size, (255, 255, 255, 0))
62-
result.paste(certificate_template, (0, 0))
63-
result = Image.alpha_composite(result, overlay)
64-
65-
return result
68+
"""Generate a certificate for a participant."""
69+
try:
70+
# Create transparent layer for text and logo
71+
overlay = Image.new("RGBA", certificate_template.size, (255, 255, 255, 0))
72+
73+
# Optimize logo size
74+
logo_size = (150, 150)
75+
logo = logo.resize(logo_size, Image.Resampling.LANCZOS)
76+
77+
# Paste logo
78+
padding = 50
79+
overlay.paste(logo, (padding, padding), logo)
80+
81+
# Add name
82+
name_image = self.create_name_image(participant.name_completed(), certificate_template.size)
83+
overlay.paste(name_image, (0, 0), name_image)
84+
85+
# Add details
86+
details_image = self.create_details_image(participant.certificate.details, certificate_template.size)
87+
name_center_y = certificate_template.size[1] // 2
88+
details_y = name_center_y + 50
89+
90+
details_with_position = Image.new("RGBA", certificate_template.size, (255, 255, 255, 0))
91+
details_with_position.paste(details_image, (0, details_y), details_image)
92+
overlay = Image.alpha_composite(overlay, details_with_position)
93+
94+
# Add validation code
95+
validation_code_image = self.create_validation_code_image(participant.formated_validation_code(), certificate_template.size)
96+
overlay.paste(validation_code_image, (0, 0), validation_code_image)
97+
98+
# Merge and optimize final image
99+
result = Image.new("RGBA", certificate_template.size, (255, 255, 255, 0))
100+
result.paste(certificate_template, (0, 0))
101+
result = Image.alpha_composite(result, overlay)
102+
103+
return result
104+
except Exception as e:
105+
logger.error(f"Erro ao gerar certificado: {str(e)}")
106+
raise
66107

67108
def create_name_image(self, name: str, size: tuple) -> Image:
68-
name_image = Image.new("RGBA", size, (255, 255, 255, 0))
69-
draw = ImageDraw.Draw(name_image)
70-
font = ImageFont.truetype(FONT_NAME, 70)
71-
position = self.calculate_text_position(name, font, draw, size)
72-
draw.text(position, name, fill=TEXT_COLOR, font=font)
73-
return name_image
109+
"""Create image with participant's name."""
110+
try:
111+
name_image = Image.new("RGBA", size, (255, 255, 255, 0))
112+
draw = ImageDraw.Draw(name_image)
113+
font = ImageFont.truetype(FONT_NAME, 70)
114+
position = self.calculate_text_position(name, font, draw, size)
115+
draw.text(position, name, fill=TEXT_COLOR, font=font)
116+
return name_image
117+
except Exception as e:
118+
logger.error(f"Erro ao criar imagem do nome: {str(e)}")
119+
raise
74120

75121
def create_details_image(self, details: str, size: tuple) -> Image:
76-
details_image = Image.new("RGBA", size, (255, 255, 255, 0))
77-
draw = ImageDraw.Draw(details_image)
78-
font = ImageFont.truetype(DETAILS_FONT, 18)
122+
"""Create image with certificate details."""
123+
try:
124+
details_image = Image.new("RGBA", size, (255, 255, 255, 0))
125+
draw = ImageDraw.Draw(details_image)
126+
font = ImageFont.truetype(DETAILS_FONT, 18)
79127

80-
# Split text into three lines
81-
words = details.split()
82-
total_words = len(words)
83-
words_per_line = total_words // 3
84-
85-
# Create three balanced lines
86-
line1 = ' '.join(words[:words_per_line])
87-
line2 = ' '.join(words[words_per_line:words_per_line*2])
88-
line3 = ' '.join(words[words_per_line*2:])
89-
90-
# Calculate positions for all three lines
91-
line_height = font.size + 10 # Add 10 pixels spacing between lines
92-
93-
# Get the width of the longest line to center all lines
94-
line1_bbox = draw.textbbox((0, 0), line1, font=font)
95-
line2_bbox = draw.textbbox((0, 0), line2, font=font)
96-
line3_bbox = draw.textbbox((0, 0), line3, font=font)
97-
98-
# Calculate vertical center position for all three lines
99-
total_height = line_height * 3
100-
start_y = 0 # Start from top since position will be handled in generate_certificate
101-
102-
# Draw each line centered
103-
x1 = (size[0] - (line1_bbox[2] - line1_bbox[0])) / 2
104-
x2 = (size[0] - (line2_bbox[2] - line2_bbox[0])) / 2
105-
x3 = (size[0] - (line3_bbox[2] - line3_bbox[0])) / 2
106-
107-
# Draw text with explicit black color
108-
draw.text((x1, start_y), line1, fill=TEXT_COLOR, font=font)
109-
draw.text((x2, start_y + line_height), line2, fill=TEXT_COLOR, font=font)
110-
draw.text((x3, start_y + line_height * 2), line3, fill=TEXT_COLOR, font=font)
111-
112-
return details_image
128+
words = details.split()
129+
total_words = len(words)
130+
words_per_line = total_words // 3
131+
132+
line1 = ' '.join(words[:words_per_line])
133+
line2 = ' '.join(words[words_per_line:words_per_line*2])
134+
line3 = ' '.join(words[words_per_line*2:])
135+
136+
line_height = font.size + 10
137+
138+
line1_bbox = draw.textbbox((0, 0), line1, font=font)
139+
line2_bbox = draw.textbbox((0, 0), line2, font=font)
140+
line3_bbox = draw.textbbox((0, 0), line3, font=font)
141+
142+
start_y = 0
143+
144+
x1 = (size[0] - (line1_bbox[2] - line1_bbox[0])) / 2
145+
x2 = (size[0] - (line2_bbox[2] - line2_bbox[0])) / 2
146+
x3 = (size[0] - (line3_bbox[2] - line3_bbox[0])) / 2
147+
148+
draw.text((x1, start_y), line1, fill=TEXT_COLOR, font=font)
149+
draw.text((x2, start_y + line_height), line2, fill=TEXT_COLOR, font=font)
150+
draw.text((x3, start_y + line_height * 2), line3, fill=TEXT_COLOR, font=font)
151+
152+
return details_image
153+
except Exception as e:
154+
logger.error(f"Erro ao criar imagem dos detalhes: {str(e)}")
155+
raise
156+
157+
def create_validation_code_image(self, validation_code: str, size: tuple) -> Image:
158+
"""Create image with validation code."""
159+
try:
160+
validation_code_image = Image.new("RGBA", size, (255, 255, 255, 0))
161+
draw = ImageDraw.Draw(validation_code_image)
162+
font = ImageFont.truetype(VALIDATION_CODE, 20)
163+
position = self.calculate_validation_code_position(validation_code, font, draw, size)
164+
draw.text(position, validation_code, fill=TEXT_COLOR, font=font)
165+
return validation_code_image
166+
except Exception as e:
167+
logger.error(f"Erro ao criar imagem do código de validação: {str(e)}")
168+
raise
113169

114170
def calculate_text_position(self, text: str, font: ImageFont, draw: ImageDraw, size: tuple) -> tuple:
171+
"""Calculate centered position for text."""
115172
text_bbox = draw.textbbox((0, 0), text, font=font)
116173
text_width = text_bbox[2] - text_bbox[0]
117174
text_height = text_bbox[3] - text_bbox[1]
118-
position = ((size[0] - text_width) / 2, (size[1] - text_height) / 2)
119-
return position
175+
return ((size[0] - text_width) / 2, (size[1] - text_height) / 2)
120176

121-
def create_validation_code_image(self, validation_code: str, size: tuple) -> Image:
122-
validation_code_image = Image.new("RGBA", size, (255, 255, 255, 0))
123-
draw = ImageDraw.Draw(validation_code_image)
124-
font = ImageFont.truetype(VALIDATION_CODE, 20)
125-
position = self.calculate_validation_code_position(validation_code, font, draw, size)
126-
draw.text(position, validation_code, fill=TEXT_COLOR, font=font)
127-
return validation_code_image
128-
129177
def calculate_validation_code_position(self, validation_code: str, font: ImageFont, draw: ImageDraw, size: tuple) -> tuple:
178+
"""Calculate position for validation code."""
130179
text_bbox = draw.textbbox((0, 0), validation_code, font=font)
131180
text_width = text_bbox[2] - text_bbox[0]
132181
text_height = text_bbox[3] - text_bbox[1]
133-
position = (size[0] - text_width - 50, size[1] - text_height - 40)
134-
return position
135-
136-
def save_certificate(self, certificate: Image, participant: Participant):
137-
name_certificate = participant.create_name_certificate()
138-
image_buffer = BytesIO()
139-
certificate.save(image_buffer, format="PNG")
140-
certificate.save(f"certificates/{name_certificate}")
141-
image_buffer.seek(0)
142-
# self.events_api.save_certificate(image_buffer, participant, name_certificate)
182+
return (size[0] - text_width - 50, size[1] - text_height - 40)
183+
184+
def save_certificate(self, certificate: Image, participant: Participant) -> str:
185+
"""Save certificate to temporary directory."""
186+
try:
187+
name_certificate = participant.create_name_certificate()
188+
file_path = os.path.join(self.temp_dir, name_certificate)
189+
190+
# Optimize image before saving
191+
certificate = certificate.convert('RGB')
192+
certificate.save(file_path, format="PNG", optimize=True)
193+
194+
return file_path
195+
except Exception as e:
196+
logger.error(f"Erro ao salvar certificado: {str(e)}")
197+
raise

0 commit comments

Comments
 (0)