33from models .participant import Participant
44from PIL import Image , ImageDraw , ImageFont
55from io import BytesIO
6+ import os
67from 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
1415logger = logging .getLogger (__name__ )
1516
1617class 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