diff --git a/Config.py b/Config.py index c77e5ca..bfbd796 100644 --- a/Config.py +++ b/Config.py @@ -3,7 +3,7 @@ TEST = False -EDITION='2024' +EDITION='2025' EDITIONS_FOLDER = 'editions' DB_PATH_T = 'hackeps-'+EDITION+'/dev/users' @@ -34,11 +34,12 @@ BOLD_FONT_FILE = 'SpaceMono-Bold.ttf' FONT_PATH = path.join(RES_PATH, FONT_FOLDER, FONT_FILE) BOLD_FONT_PATH = path.join(RES_PATH, FONT_FOLDER, BOLD_FONT_FILE) -TYPE_FONT_SIZE = 150 -NAME_FONT_SIZE = 249 +TYPE_FONT_SIZE = 40 +NAME_FONT_SIZE = 60 FONT_COLOR = (0,0,0) -WHITE_FONT_COLOR = (255,255,255) +WHITE_FONT_COLOR = (84,49,26) DARK_FONT_COLOR = (35,35,35) +BROWN_COLOR = (101, 67, 33) TYPE_FONT = ImageFont.truetype(FONT_PATH, TYPE_FONT_SIZE) NAME_FONT = ImageFont.truetype(FONT_PATH, NAME_FONT_SIZE) BOLD_TYPE_FONT = ImageFont.truetype(BOLD_FONT_PATH, TYPE_FONT_SIZE) @@ -46,4 +47,4 @@ # FONT = ImageFont.truetype("Symbola.ttf", 60, encoding='unic') MAIN_COLOR = (31, 33, 36) -BAK_COLOR = (196, 222, 211) \ No newline at end of file +BAK_COLOR = (119, 177, 201) \ No newline at end of file diff --git a/batch_pdfer.py b/batch_pdfer.py index d41448b..936ef4d 100644 --- a/batch_pdfer.py +++ b/batch_pdfer.py @@ -3,35 +3,87 @@ from fpdf import FPDF import glob import os - image_directory = r'./_out_/' extensions = ('*.jpg','*.png','*.gif') + +# --- Configurable layout / scaling parameters --- +# Convert pixels to millimeters. Increase this to make images larger. +# Typical values: 0.0271 (small), 0.0847 (larger). Default here is 0.0847 for bigger images. +PIXEL_TO_MM = 0.0847 +# Page layout: number of images per row/column +IMAGES_PER_ROW = 3 +IMAGES_PER_COL = 3 +# Page margins (mm) and gap between images (mm) +MARGIN_MM = 0 +GAP_MM = 1 +# Target badge size (mm) — set to your measured lanyard size +TARGET_WIDTH_MM = 95 +TARGET_HEIGHT_MM = 70 +# If True, compute IMAGES_PER_ROW / IMAGES_PER_COL automatically based on the target and page size +AUTO_LAYOUT = False +# How much of the target area the image should fill (0.0-1.0) +CELL_FILL = 0.95 +# Allow upscaling images if they're smaller than the target +UPSCALE = True +# Global multiplier for fine adjustments +GLOBAL_SCALE = 1.0 +# ------------------------------------------------- + pdf = FPDF() -imagelist=[] +imagelist = [] for ext in extensions: - imagelist.extend(glob.glob(os.path.join(image_directory,ext))) + imagelist.extend(glob.glob(os.path.join(image_directory, ext))) + +pdf_size = {'P': {'w': 210, 'h': 297}, 'L': {'w': 297, 'h': 210}} for index, imageFile in enumerate(imagelist): cover = Image.open(imageFile) - width, height = cover.size + px_w, px_h = cover.size + + # convert pixels to mm + width_mm = float(px_w * PIXEL_TO_MM) + height_mm = float(px_h * PIXEL_TO_MM) - # convert pixel in mm with 1px=0.0847 mm - width, height = float(width * 0.0271), float(height * 0.0271) + # determine orientation by image aspect + orientation = 'P' if width_mm < height_mm else 'L' - # given we are working with A4 format size - pdf_size = {'P': {'w': 210, 'h': 297}, 'L': {'w': 297, 'h': 210}} + page_w = pdf_size[orientation]['w'] + page_h = pdf_size[orientation]['h'] - # get page orientation from image size - orientation = 'P' if width < height else 'L' + # If AUTO_LAYOUT and target specified, compute how many target cells fit on the page + if AUTO_LAYOUT and TARGET_WIDTH_MM and TARGET_HEIGHT_MM: + IMAGES_PER_ROW = max(1, int((page_w - 2 * MARGIN_MM + GAP_MM) // (TARGET_WIDTH_MM + GAP_MM))) + IMAGES_PER_COL = max(1, int((page_h - 2 * MARGIN_MM + GAP_MM) // (TARGET_HEIGHT_MM + GAP_MM))) - # make sure image size is not greater than the pdf format size - width = width if width < pdf_size[orientation]['w'] else pdf_size[orientation]['w'] - height = height if height < pdf_size[orientation]['h'] else pdf_size[orientation]['h'] + # compute available cell size for each image + cell_w = (page_w - 2 * MARGIN_MM - (IMAGES_PER_ROW - 1) * GAP_MM) / IMAGES_PER_ROW + cell_h = (page_h - 2 * MARGIN_MM - (IMAGES_PER_COL - 1) * GAP_MM) / IMAGES_PER_COL - if index % 9 == 0: + # If a target size is set, try to fit the image inside the target area first + if TARGET_WIDTH_MM and TARGET_HEIGHT_MM: + target_w = TARGET_WIDTH_MM * GLOBAL_SCALE * CELL_FILL + target_h = TARGET_HEIGHT_MM * GLOBAL_SCALE * CELL_FILL + base_scale = min(target_w / width_mm, target_h / height_mm) + else: + base_scale = min(cell_w / width_mm, cell_h / height_mm) + + if not UPSCALE: + base_scale = min(1.0, base_scale) + + render_w = width_mm * base_scale + render_h = height_mm * base_scale + + # add new page when needed + images_per_page = IMAGES_PER_ROW * IMAGES_PER_COL + if index % images_per_page == 0: pdf.add_page(orientation=orientation) - pdf.image(imageFile, 7+(width+1) * (index%3), 7+(height+1)*((index//3)%3), width, height) + col = index % IMAGES_PER_ROW + row = (index // IMAGES_PER_ROW) % IMAGES_PER_COL + + x = MARGIN_MM + (render_w + GAP_MM) * col + y = MARGIN_MM + (render_h + GAP_MM) * row + pdf.image(imageFile, x, y, render_w, render_h) -pdf.output(image_directory + "file.pdf", "F") \ No newline at end of file +pdf.output(os.path.join(image_directory, "file.pdf"), "F") \ No newline at end of file diff --git a/models/assistant.py b/models/assistant.py index 55f4c8e..c5df194 100644 --- a/models/assistant.py +++ b/models/assistant.py @@ -35,7 +35,7 @@ def generate_qr(self, crypt_id=False): def generate_card(self, rgb_back=(255, 255, 255), template=Config.BAK_PATH_CONTESTANT): self.card = Image.open(template) - tools.draw_text(self.card, self.type, Card.TYPE_POS, Config.TYPE_FONT, Config.WHITE_FONT_COLOR) + tools.draw_text(self.card, self.type, Card.TYPE_POS, Config.TYPE_FONT, Config.BROWN_COLOR) if self.type == '': self.smallen() diff --git a/models/card.py b/models/card.py index 99a2e78..eac51f0 100644 --- a/models/card.py +++ b/models/card.py @@ -1,9 +1,9 @@ class Card: QR_PIX_SIZE = 1 - QR_POS = (711*3, 315*3-333//3) - _QR_SIZE = 331*3 + QR_POS = (711*1 - 100, 315*2-900//3) + _QR_SIZE = 331*1 QR_SIZE = (_QR_SIZE, _QR_SIZE) QR_BORDER_SIZE = 2 - TYPE_POS = (1050*3, 150*3) - NAME_POS = (1050*3, 40*3) - NICK_POS = (984*3, 40*3) \ No newline at end of file + TYPE_POS = (1063*(1/2), 720*(0.8/5)) + NAME_POS = (1063*(1/2), 720*(0.2/5)) + NICK_POS = (1063*(2/4), 720*(2/5)) \ No newline at end of file diff --git a/models/company.py b/models/company.py index fe40023..208d638 100644 --- a/models/company.py +++ b/models/company.py @@ -22,12 +22,13 @@ def __init__(self, name:str, image): self.logo = image def generate_card(self, rgb_back=(255, 255, 255)): + print("Generating card for company:", self.name) self.card = Image.open(Config.BAK_PATH_EMPRESA) tools.draw_text(self.card, self.type, Card.TYPE_POS, Config.TYPE_FONT, Config.DARK_FONT_COLOR) if self.type == '': self.smallen() image = tools.translate_image(self.logo) - image = tools.scale(image, Card.QR_SIZE, mask_col=(255,204,77)) + image = tools.scale(image, Card.QR_SIZE) self.card.paste(image, Card.QR_POS) tools.centrate_text_relative(self.card, self.name.strip(),Config.BOLD_NAME_FONT, Card.NAME_POS, Card.QR_SIZE * 3, Config.DARK_FONT_COLOR) self.smallen() @@ -55,4 +56,14 @@ def get_data(name=None): break if Config.TEST or (name is not None and comp['name'] == name): break + + tier3=get_by_tier(3) + for comp in tier3: + for _ in range(5): + if name is None or comp['name'] == name: + res.append(Company(comp['name'], comp['image'])) + if Config.TEST: + break + if Config.TEST or (name is not None and comp['name'] == name): + break return res \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3ce8d51..59ba41e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,11 +13,11 @@ google-cloud-storage==2.8.0 google-resumable-media==2.4.1 googleapis-common-protos==1.59.0 # grpcio==1.25.0 -grpcio==1.53.0 +grpcio==1.76.0 httplib2==0.22.0 idna==3.4 msgpack==1.0.5 -Pillow==9.5.0 +Pillow==12.0.0 # pkg-resources==0.0.0 # protobuf==3.15.0 protobuf==4.22.1 @@ -29,4 +29,5 @@ requests==2.28.2 rsa==4.9 six==1.16.0 uritemplate==3.0.0 -urllib3==1.26.15 \ No newline at end of file +urllib3==1.26.15 +cairosvg>=2.0.0 \ No newline at end of file diff --git a/resources/editions/2025/images/agrotecnio.png b/resources/editions/2025/images/agrotecnio.png new file mode 100644 index 0000000..e2be4a3 Binary files /dev/null and b/resources/editions/2025/images/agrotecnio.png differ diff --git a/resources/editions/2025/images/bonarea.png b/resources/editions/2025/images/bonarea.png new file mode 100644 index 0000000..adb7996 Binary files /dev/null and b/resources/editions/2025/images/bonarea.png differ diff --git a/resources/editions/2025/images/eurecat.png b/resources/editions/2025/images/eurecat.png new file mode 100644 index 0000000..846a6c5 Binary files /dev/null and b/resources/editions/2025/images/eurecat.png differ diff --git a/resources/editions/2025/images/ingroup.png b/resources/editions/2025/images/ingroup.png new file mode 100644 index 0000000..750da9a Binary files /dev/null and b/resources/editions/2025/images/ingroup.png differ diff --git a/resources/editions/2025/images/leitat.png b/resources/editions/2025/images/leitat.png new file mode 100644 index 0000000..ea5723d Binary files /dev/null and b/resources/editions/2025/images/leitat.png differ diff --git a/resources/editions/2025/images/logogran.png b/resources/editions/2025/images/logogran.png new file mode 100644 index 0000000..7d8aa36 Binary files /dev/null and b/resources/editions/2025/images/logogran.png differ diff --git a/resources/editions/2025/images/paeria.png b/resources/editions/2025/images/paeria.png new file mode 100644 index 0000000..90c91ed Binary files /dev/null and b/resources/editions/2025/images/paeria.png differ diff --git a/resources/editions/2025/images/restbai.png b/resources/editions/2025/images/restbai.png new file mode 100644 index 0000000..cf2ffe1 Binary files /dev/null and b/resources/editions/2025/images/restbai.png differ diff --git a/resources/editions/2025/images/udl.png b/resources/editions/2025/images/udl.png new file mode 100644 index 0000000..aeb6971 Binary files /dev/null and b/resources/editions/2025/images/udl.png differ diff --git a/resources/editions/2025/plantilles/mentor.png b/resources/editions/2025/plantilles/mentor.png new file mode 100644 index 0000000..23b8721 Binary files /dev/null and b/resources/editions/2025/plantilles/mentor.png differ diff --git a/resources/editions/2025/plantilles/organitzador.png b/resources/editions/2025/plantilles/organitzador.png new file mode 100644 index 0000000..1c464cd Binary files /dev/null and b/resources/editions/2025/plantilles/organitzador.png differ diff --git a/resources/editions/2025/plantilles/participant.png b/resources/editions/2025/plantilles/participant.png new file mode 100644 index 0000000..7c75300 Binary files /dev/null and b/resources/editions/2025/plantilles/participant.png differ diff --git a/resources/editions/2025/plantilles/patrocinador.png b/resources/editions/2025/plantilles/patrocinador.png new file mode 100644 index 0000000..56f0d63 Binary files /dev/null and b/resources/editions/2025/plantilles/patrocinador.png differ diff --git a/resources/editions/data_sample.json b/resources/editions/data_sample.json index 5cc3461..632f4c1 100644 --- a/resources/editions/data_sample.json +++ b/resources/editions/data_sample.json @@ -1,7 +1,32 @@ { "empty": 5, "organizers": [ - {"name":"Ton Llucià Senserrich"} + {"name":"Pol Llinàs"}, + {"name":"Naïm Saadi"}, + {"name":"Lluc Vivet"}, + {"name":"Lotfi Bouakel (LOLO)"}, + {"name":"Ton Llucià "}, + {"name":"Joel Ros "}, + {"name":"Gerard Tersa "}, + {"name":"Alba Serrano "}, + {"name":"Aida Chavero "}, + {"name":"Martina López "}, + {"name":"Anaïs Garrofé "}, + {"name":"Júlia Calvo "}, + {"name":"Aleix Rosinach "}, + {"name":"Andreu Puig-gròs "}, + {"name":"Enric Zhang "}, + {"name":"David Garcia "}, + {"name":"Nataly Jaya "}, + {"name":"Toni López "}, + {"name":"Òscar van de Crommert"}, + {"name":"Carles Sànchez"}, + {"name":"Gerard Ballebrera"}, + {"name":"Albert Sorribes"}, + {"name":"Bru Pallàs "}, + {"name":"Norbert Aguilera"}, + {"name":"Aleix Bertran"} + ], "volunteers": [ {"name":"Ton Llucià Senserrich"} @@ -11,5 +36,18 @@ ], "guests": [ {"name":"Ton Llucià Senserrich","type":"HACKER","qr":true} + ], + "mentors": [ + {"name":"David Sarrat","qr":true}, + {"name":"Sergi Vila","qr":true}, + {"name":"Alba Lamas","qr":true}, + {"name":"Josep","qr":true}, + {"name":"Jordi Onrubia","qr":true}, + {"name":"Cangrejo #1"}, + {"name":"Cangrejo #2"}, + {"name":"Cangrejo #3"}, + {"name":"ivansaureu15"}, + {"name":"Josep Maria Salvia"}, + {"name":"Salce"} ] } diff --git a/tools.py b/tools.py index 4f9f8b1..72c1368 100644 --- a/tools.py +++ b/tools.py @@ -36,19 +36,21 @@ def draw_text(image, text, pos, font, fill, centrate=True, mayus=False): text = text.upper() draw = ImageDraw.Draw(image) w = draw.textlength(text, font=font) - # w, h = draw.textsize(text, font=font) + # Center horizontally around pos[0] when centrate=True if centrate: - ImageDraw.Draw(image).text((pos[0]-w, pos[1]), text, font=font,fill=fill) + x = pos[0] - (w / 2) + ImageDraw.Draw(image).text((x, pos[1]), text, font=font, fill=fill) else: - ImageDraw.Draw(image).text(pos, text, font=font,fill=fill) + ImageDraw.Draw(image).text(pos, text, font=font, fill=fill) def centrate_text_relative(image, text, font, relative_pos, relative_size, fill, mayus=False): #centrate relative on x and split in 2 lines if text width is bigger than relative_size if mayus: text = text.upper() draw = ImageDraw.Draw(image) - w= draw.textlength(text, font=font) - x = relative_pos[0] - w + w = draw.textlength(text, font=font) + # center the text horizontally around relative_pos[0] + x = relative_pos[0] - (w / 2) y = relative_pos[1] # if w > relative_size[0]: # #split in 2 lines @@ -154,8 +156,94 @@ def clear_cache(): def translate_image(image) -> Image: - if image[:4] == "http": - return Image.open(BytesIO(requests.get(image).content)) - else: - base64 = b64decode(image.split(",")[-1]) - return Image.open(BytesIO(base64)) \ No newline at end of file + # handle bytes directly + if isinstance(image, (bytes, bytearray)): + return Image.open(BytesIO(image)) + + s = str(image) + # remote URL + if s[:4].lower() == "http": + resp = requests.get(s, timeout=10) + try: + resp.raise_for_status() + except Exception as e: + raise RuntimeError(f"Failed to download image from URL {s}: {e}") + + ct = resp.headers.get('Content-Type', '').lower() + content = resp.content + + # handle SVG responses specifically (PIL doesn't support SVG) + if 'svg' in ct or (content.lstrip().startswith(b'<') and b'