Compare commits

..

32 Commits

Author SHA1 Message Date
Shiro-Nek0
85b2c0b4db more theme and camera fixes TwT 2026-02-27 01:11:54 -03:00
Shiro-Nek0
741690b30e X fix 2026-02-27 01:06:42 -03:00
Shiro-Nek0
7235c7ff09 theme and camera cookie 2026-02-27 00:57:45 -03:00
Shiro-Nek0
8cba7937c3 image cache fix 2026-02-27 00:29:25 -03:00
Shiro-Nek0
4779452acd image timestamp fix 2026-02-27 00:16:43 -03:00
Shiro-Nek0
600df52b04 camera index? 2026-02-27 00:03:55 -03:00
Shiro-Nek0
b1b99bc887 webui camera barcode scanner 2026-02-26 23:59:22 -03:00
Shiro-Nek0
c7c0b3feb2 picture qol fixes 2026-02-26 23:55:49 -03:00
Shiro-Nek0
184f2722bf sort table 2026-02-26 23:38:55 -03:00
Shiro-Nek0
0f9966d224 qol and scan fixes 2026-02-26 23:29:45 -03:00
Shiro-Nek0
43b2a9e2d5 version bump :p 2026-02-26 22:53:44 -03:00
Shiro-Nek0
3a39cb95db re-retriede image if cached one doesnt exist anymore 2026-02-26 22:52:34 -03:00
Shiro-Nek0
81cacd3589 bulk delete + more prompts 2026-02-26 22:50:20 -03:00
Shiro-Nek0
1f521ec1d2 colors and prompts fix 2026-02-26 22:33:38 -03:00
0dcf0bc930 modified: static/styleIndex.css
modified:   templates/index.html
2026-02-26 21:46:42 -03:00
df4ff9171d Merge branch 'main' of https://gitea.sekidesu.xyz/SekiDesu01/SekiPOS 2026-02-26 21:16:41 -03:00
1b2e63bc86 added bulk price update feature, allowing users to select multiple products and apply a new price to all of them at once. The bulk action bar now shows the count of selected items and enables the "OK" button only when at least one product is selected. 2026-02-26 21:16:16 -03:00
Shiro-Nek0
80bf539484 Merge branch 'main' of https://gitea.sekidesu.xyz/SekiDesu01/SekiPOS 2026-02-26 20:48:56 -03:00
13bba33c26 modified: templates/index.html 2026-02-26 04:21:20 -03:00
ecd98c72ce new file: ScannerPython/output/scannerV2.exe
new file:   ScannerPython/scannerV2.py
2026-02-26 04:10:48 -03:00
Shiro-Nek0
344229b77b bump version 2026-02-26 03:35:34 -03:00
Shiro-Nek0
9a28daa2cd fill edit fileds on scan 2026-02-26 03:33:45 -03:00
Shiro-Nek0
ebf8bc72aa fix custom images cache 2026-02-26 03:30:23 -03:00
Shiro-Nek0
2ef510358d cache image not being used fixed 2026-02-26 03:22:05 -03:00
Shiro-Nek0
d492905e57 todos 2026-02-26 03:06:07 -03:00
Shiro-Nek0
7a4d122976 file organization of htmls css 2026-02-26 03:02:03 -03:00
Shiro-Nek0
8cc5138888 discord theme, top bar fix, organized folders 2026-02-26 02:55:20 -03:00
3f47b3cda4 new file: KeyGenerator/A4 Printable Grid.py
new file:   keychain_3x3_perfect.pdf
2026-02-26 02:39:05 -03:00
Shiro-Nek0
c1045b4878 path python fix 2026-02-26 02:37:07 -03:00
Shiro-Nek0
70c14acaa5 dark mode + reorganization 2026-02-26 02:29:23 -03:00
2701dfbf85 Merge branch 'main' of https://gitea.sekidesu.xyz/SekiDesu01/SekiPOS 2026-02-26 02:26:09 -03:00
6aa3421f0c tajetitas 2026-02-26 02:25:48 -03:00
190 changed files with 5042 additions and 280 deletions

View File

@@ -0,0 +1,70 @@
import os
from PIL import Image
# --- CONFIGURATION ---
CARD_DIR = os.path.join(os.getcwd(),'keychain_cards')
OUTPUT_PDF = os.path.join(os.getcwd(), 'keychain_3x3_perfect.pdf')
# A4 at 300 DPI
PAGE_W, PAGE_H = 2480, 3508
COLS, ROWS = 3, 3 # 9 cards per page
PAGE_MARGIN = 150
def generate_printable_pdf():
all_files = [f for f in os.listdir(CARD_DIR) if f.endswith('.png')]
if not all_files:
print("❌ No cards found.")
return
all_files.sort()
# Calculate slot size
available_w = PAGE_W - (PAGE_MARGIN * 2)
available_h = PAGE_H - (PAGE_MARGIN * 2)
slot_w = available_w // COLS
slot_h = available_h // ROWS
# TARGET CALCULATION (Maintain 300:450 ratio)
# We fit to the width of the slot and let height follow
target_w = int(slot_w * 0.9)
target_h = int(target_w * (450 / 300)) # Maintain original 1:1.5 ratio
# Safety check: if height is too big for slot, scale down based on height instead
if target_h > (slot_h * 0.9):
target_h = int(slot_h * 0.9)
target_w = int(target_h * (300 / 450))
pages = []
current_page = Image.new('RGB', (PAGE_W, PAGE_H), 'white')
print(f"📄 Generating {COLS}x{ROWS} grid. {len(all_files)} cards total.")
for i, filename in enumerate(all_files):
item_idx = i % (COLS * ROWS)
# New page logic
if item_idx == 0 and i > 0:
pages.append(current_page)
current_page = Image.new('RGB', (PAGE_W, PAGE_H), 'white')
row = item_idx // COLS
col = item_idx % COLS
img_path = os.path.join(CARD_DIR, filename)
card_img = Image.open(img_path).convert('RGB')
# Resize using the aspect-ratio-safe dimensions
card_img = card_img.resize((target_w, target_h), Image.Resampling.LANCZOS)
# Center in slot
x = PAGE_MARGIN + (col * slot_w) + (slot_w - target_w) // 2
y = PAGE_MARGIN + (row * slot_h) + (slot_h - target_h) // 2
current_page.paste(card_img, (x, y))
pages.append(current_page)
pages[0].save(OUTPUT_PDF, save_all=True, append_images=pages[1:], resolution=300.0, quality=100)
print(f"✅ Created {OUTPUT_PDF}. Now go print it and stop crying.")
if __name__ == "__main__":
generate_printable_pdf()

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
import pandas as pd
import json
import os
file_path = os.path.join(os.getcwd(), 'PLU+FSMA+list+v1.0.xlsx')
sheet_name = 'Non FTL'
new_url_base = "https://server-ifps.accurateig.com/assets/commodities/"
def get_one_of_each():
if not os.path.exists(file_path):
print("❌ Excel file not found.")
return
# 1. Load Excel
df = pd.read_excel(file_path, sheet_name=sheet_name)
# 2. Drop rows missing the essentials
df = df.dropna(subset=['IMAGE', 'PLU', 'COMMODITY'])
# 3. CRITICAL: Drop duplicates by COMMODITY only
# This ignores Variety and Size, giving us exactly one row per fruit type.
df_unique = df.drop_duplicates(subset=['COMMODITY'], keep='first')
data_output = []
for _, row in df_unique.iterrows():
# Extract filename from the messy URL in Excel
original_link = str(row['IMAGE'])
filename = original_link.split('/')[-1]
# Build the final working URL
image_url = f"{new_url_base}{filename}"
# Get the clean Commodity name
commodity = str(row['COMMODITY']).title()
plu_code = str(row['PLU'])
data_output.append({
"name": commodity,
"plu": plu_code,
"image": image_url
})
# 4. Save to JSON
with open('one_of_each.json', 'w', encoding='utf-8') as f:
json.dump(data_output, f, indent=4, ensure_ascii=False)
print(f"✅ Success! Generated 'one_of_each.json' with {len(data_output)} unique commodities.")
if __name__ == "__main__":
get_one_of_each()

437
KeyGenerator/frutas.json Normal file
View File

@@ -0,0 +1,437 @@
[
{
"name": "Almendras",
"plu": "4924",
"image": "https://server-ifps.accurateig.com/assets/commodities/4924-almonds_1630598936.jpg"
},
{
"name": "Manzanas",
"plu": "4099",
"image": "https://server-ifps.accurateig.com/assets/commodities/apples-akane_1629314651.png"
},
{
"name": "Damascos",
"plu": "3044",
"image": "https://server-ifps.accurateig.com/assets/commodities/3044-apricots-black-velvet-03_1614619576.jpg"
},
{
"name": "Alcachofas",
"plu": "4519",
"image": "https://server-ifps.accurateig.com/assets/commodities/4519-artichokes-baby-02_1625671366.jpg"
},
{
"name": "Espárragos",
"plu": "4521",
"image": "https://server-ifps.accurateig.com/assets/commodities/4521-asparagus-01_1625671446.jpg"
},
{
"name": "Paltas",
"plu": "3509",
"image": "https://server-ifps.accurateig.com/assets/commodities/3509-gem-avocado_1625011346.JPG"
},
{
"name": "Babaco",
"plu": "3303",
"image": "https://server-ifps.accurateig.com/assets/commodities/3303-babacoa_1614719281.JPG"
},
{
"name": "Plátanos",
"plu": "4235",
"image": "https://server-ifps.accurateig.com/assets/commodities/4235-plantains-01_1625076376.jpg"
},
{
"name": "Porotos verdes / Porotos",
"plu": "4527",
"image": "https://server-ifps.accurateig.com/assets/commodities/4527-beans-chinese-long-07_1625671743.jpg"
},
{
"name": "Betarragas",
"plu": "4537",
"image": "https://server-ifps.accurateig.com/assets/commodities/4537-baby-golden-beets_1635173500.jpg"
},
{
"name": "Moras / Berries",
"plu": "4239",
"image": "https://server-ifps.accurateig.com/assets/commodities/4239-berries-blackberries-01_1625076478.jpg"
},
{
"name": "Nueces de Brasil",
"plu": "4926",
"image": "https://server-ifps.accurateig.com/assets/commodities/4926-brazil-nuts-02_1625756785.JPG"
},
{
"name": "Repollo",
"plu": "4069",
"image": "https://server-ifps.accurateig.com/assets/commodities/4069-green-cabbage_1633958066.jpg"
},
{
"name": "Zanahorias",
"plu": "4560",
"image": "https://server-ifps.accurateig.com/assets/commodities/4560-carrots-baby_1625673556.jpg"
},
{
"name": "Castañas de cajú",
"plu": "3105",
"image": "https://server-ifps.accurateig.com/assets/commodities/3105-cashews-03_1614635878.JPG"
},
{
"name": "Apionabo",
"plu": "3321",
"image": "https://server-ifps.accurateig.com/assets/commodities/3321-celery-root-03_1614720213.jpg"
},
{
"name": "Cerezas",
"plu": "3549",
"image": "https://server-ifps.accurateig.com/assets/commodities/screenshot-2023-05-02-100428_1683036305.png"
},
{
"name": "Castañas",
"plu": "4927",
"image": "https://server-ifps.accurateig.com/assets/commodities/4927-chestnuts-italian-02_1625756943.jpg"
},
{
"name": "Cocos",
"plu": "4261",
"image": "https://server-ifps.accurateig.com/assets/commodities/4261-coconuts-02_1625077777.jpg"
},
{
"name": "Choclo",
"plu": "3087",
"image": "https://server-ifps.accurateig.com/assets/commodities/3087-corn-04_1614633780.jpg"
},
{
"name": "Dátiles",
"plu": "4862",
"image": "https://server-ifps.accurateig.com/assets/commodities/4862-dates_1630598790.jpg"
},
{
"name": "Eneldo",
"plu": "4892",
"image": "https://server-ifps.accurateig.com/assets/commodities/4892-dill-baby-02_1625755376.jpg"
},
{
"name": "Berenjena",
"plu": "4599",
"image": "https://server-ifps.accurateig.com/assets/commodities/4599-baby-eggplant-aubergine_1633372314.jpg"
},
{
"name": "Helechos",
"plu": "4606",
"image": "https://server-ifps.accurateig.com/assets/commodities/4606-fiddlehead-ferns-07_1625679559.jpg"
},
{
"name": "Higos",
"plu": "4266",
"image": "https://server-ifps.accurateig.com/assets/commodities/4266-figs-03_1625077969.jpg"
},
{
"name": "Ajo",
"plu": "4608",
"image": "https://server-ifps.accurateig.com/assets/commodities/4608-garlic-regular_1637184640.jpg"
},
{
"name": "Jengibre",
"plu": "4612",
"image": "https://server-ifps.accurateig.com/assets/commodities/4612-ginger-root-06_1625679690.jpg"
},
{
"name": "Bardana",
"plu": "3091",
"image": "https://server-ifps.accurateig.com/assets/commodities/3091gobo-root-02_1614634374.jpg"
},
{
"name": "Pomelo",
"plu": "4279",
"image": "https://server-ifps.accurateig.com/assets/commodities/4279-pummelos-02_1625078575.jpg"
},
{
"name": "Uvas",
"plu": "3491",
"image": "https://server-ifps.accurateig.com/assets/commodities/arra-15-grape_1518122851.JPG"
},
{
"name": "Hojas de col / Verdes",
"plu": "4614",
"image": "https://server-ifps.accurateig.com/assets/commodities/4614-collard-greens-trimmed_1625679839.JPG"
},
{
"name": "Rábano picante",
"plu": "4625",
"image": "https://server-ifps.accurateig.com/assets/commodities/4625-horseradish-root-04_1625680026.jpg"
},
{
"name": "Jícama",
"plu": "4626",
"image": "https://server-ifps.accurateig.com/assets/commodities/4626-jicama_1635179919.jpg"
},
{
"name": "Kiwi",
"plu": "3279",
"image": "https://server-ifps.accurateig.com/assets/commodities/3279-kiwi-gold-03_1614718637.jpg"
},
{
"name": "Kumquat",
"plu": "4303",
"image": "https://server-ifps.accurateig.com/assets/commodities/4303-kumquats-03_1625079229.jpg"
},
{
"name": "Puerros",
"plu": "4629",
"image": "https://server-ifps.accurateig.com/assets/commodities/4629-leeks-02_1625680225.jpg"
},
{
"name": "Limones",
"plu": "3626",
"image": "https://server-ifps.accurateig.com/assets/commodities/3626-meyer-lemons_1460404763.jpg"
},
{
"name": "Limequats",
"plu": "4328",
"image": "https://server-ifps.accurateig.com/assets/commodities/4328-limequats-01_1625081231.jpg"
},
{
"name": "Limas",
"plu": "4305",
"image": "https://server-ifps.accurateig.com/assets/commodities/4305-limes-key-03_1625079363.jpg"
},
{
"name": "Nísperos",
"plu": "4308",
"image": "https://server-ifps.accurateig.com/assets/commodities/4308-lychees-5_1625079574.jpg"
},
{
"name": "Raíz de loto",
"plu": "3099",
"image": "https://server-ifps.accurateig.com/assets/commodities/3099-lotus-root-03_1614635240.jpg"
},
{
"name": "Macadamia",
"plu": "3106",
"image": "https://server-ifps.accurateig.com/assets/commodities/3106-macadamias_1614705972.JPG"
},
{
"name": "Malanga",
"plu": "4644",
"image": "https://server-ifps.accurateig.com/assets/commodities/4644-malanga_1633956612.jpg"
},
{
"name": "Menta",
"plu": "3475",
"image": "https://server-ifps.accurateig.com/assets/commodities/peppermint-3475_1625790587.jpg"
},
{
"name": "Hongos / Callampas",
"plu": "4647",
"image": "https://server-ifps.accurateig.com/assets/commodities/4647-mushrooms-chanterelles-1_1625681503.jpg"
},
{
"name": "Ñame",
"plu": "3276",
"image": "https://server-ifps.accurateig.com/assets/commodities/3276-name-white_1634146226.jpg"
},
{
"name": "Nectarinas",
"plu": "3437",
"image": "https://server-ifps.accurateig.com/assets/commodities/yellow-fleshed-flat-nectarine_1629140965.jpg"
},
{
"name": "Cebollines / Cebollas",
"plu": "4068",
"image": "https://server-ifps.accurateig.com/assets/commodities/4068-onions-green-2_1625063600.jpg"
},
{
"name": "Naranjas",
"plu": "4381",
"image": "https://server-ifps.accurateig.com/assets/commodities/4381-oranges-blood-01_1625082045.jpg"
},
{
"name": "Chirivía",
"plu": "4672",
"image": "https://server-ifps.accurateig.com/assets/commodities/4672-parsnip_1633958885.jpg"
},
{
"name": "Maracuyá",
"plu": "3311",
"image": "https://server-ifps.accurateig.com/assets/commodities/3311-passion-fruit-02_1614719436.jpg"
},
{
"name": "Duraznos",
"plu": "3113",
"image": "https://server-ifps.accurateig.com/assets/commodities/3113-peaches-donut-04_1614707155.jpg"
},
{
"name": "Maníes",
"plu": "4931",
"image": "https://server-ifps.accurateig.com/assets/commodities/4931-peanuts_1625757234.JPG"
},
{
"name": "Peras",
"plu": "3317",
"image": "https://server-ifps.accurateig.com/assets/commodities/3317-angelys-pear_1460402454.jpg"
},
{
"name": "Arvejas",
"plu": "4673",
"image": "https://server-ifps.accurateig.com/assets/commodities/4673-peas-black-eyed-01_1625684339.jpg"
},
{
"name": "Nueces pecán",
"plu": "4936",
"image": "https://server-ifps.accurateig.com/assets/commodities/4936-pecans-01_1625757366.JPG"
},
{
"name": "Caqui",
"plu": "4428",
"image": "https://server-ifps.accurateig.com/assets/commodities/4428-persimmons-fuyu-01_1625667410.jpg"
},
{
"name": "Physalis",
"plu": "3039",
"image": "https://server-ifps.accurateig.com/assets/commodities/3039-cape-gooseberry_1630596655.jpg"
},
{
"name": "Piña",
"plu": "4430",
"image": "https://server-ifps.accurateig.com/assets/commodities/4430-pineapple-05_1625667649.jpg"
},
{
"name": "Pistacho",
"plu": "4939",
"image": "https://server-ifps.accurateig.com/assets/commodities/4939-pistachios_1625757519.JPG"
},
{
"name": "Pitahaya",
"plu": "3040",
"image": "https://server-ifps.accurateig.com/assets/commodities/3040-pitaya-dragon-fruit-01_1614282658.jpg"
},
{
"name": "Plumcot",
"plu": "3611",
"image": "https://server-ifps.accurateig.com/assets/commodities/interspecific-plum-black-picture_1629142350.JPG"
},
{
"name": "Ciruelas",
"plu": "4435",
"image": "https://server-ifps.accurateig.com/assets/commodities/4435-plums-greengage-01_1625667846.jpg"
},
{
"name": "Granada",
"plu": "3440",
"image": "https://server-ifps.accurateig.com/assets/commodities/3440-large-pomegranate_1486484749.JPG"
},
{
"name": "Papas",
"plu": "3414",
"image": "https://server-ifps.accurateig.com/assets/commodities/3414-baking-potato-white_1635179333.jpg"
},
{
"name": "Zapallo",
"plu": "4734",
"image": "https://server-ifps.accurateig.com/assets/commodities/4734-mini-pumpkin_1633964765.jpg"
},
{
"name": "Guía de zapallo",
"plu": "3480",
"image": "https://server-ifps.accurateig.com/assets/commodities/pumpkin-vine-3480_1625790222.jpg"
},
{
"name": "Quelites",
"plu": "3478",
"image": "https://server-ifps.accurateig.com/assets/commodities/quelite-3478_1625790359.jpg"
},
{
"name": "Membrillo",
"plu": "4447",
"image": "https://server-ifps.accurateig.com/assets/commodities/4447-quince-1_1625668926.jpg"
},
{
"name": "Radicchio",
"plu": "3168",
"image": "https://server-ifps.accurateig.com/assets/commodities/3168-radicchio-castelfranco-02_1614718366.jpg"
},
{
"name": "Rábanos",
"plu": "4739",
"image": "https://server-ifps.accurateig.com/assets/commodities/4739-radish-black-03_1625751544.jpg"
},
{
"name": "Ruibarbo",
"plu": "4745",
"image": "https://server-ifps.accurateig.com/assets/commodities/4745-rhubarb-01_1625751764.jpg"
},
{
"name": "Rutabagas",
"plu": "4747",
"image": "https://server-ifps.accurateig.com/assets/commodities/4747-rutabaga_1635168852.jpg"
},
{
"name": "Zapote",
"plu": "3137",
"image": "https://server-ifps.accurateig.com/assets/commodities/3137-sapote-white_1635264028.jpg"
},
{
"name": "Zapallo italiano / Zapallo",
"plu": "4750",
"image": "https://server-ifps.accurateig.com/assets/commodities/4750-squash-acorn-01_1625751871.jpg"
},
{
"name": "Caña de azúcar",
"plu": "4790",
"image": "https://server-ifps.accurateig.com/assets/commodities/4790-sugar-cane_1630598686.jpg"
},
{
"name": "Tupinambo",
"plu": "4791",
"image": "https://server-ifps.accurateig.com/assets/commodities/4791-sunchokes-03_1625753005.jpg"
},
{
"name": "Semillas de maravilla",
"plu": "4942",
"image": "https://server-ifps.accurateig.com/assets/commodities/4942-sunflower-seeds_1630599026.jpg"
},
{
"name": "Camote",
"plu": "4816",
"image": "https://server-ifps.accurateig.com/assets/commodities/4816-sweet-potatoes-02_1625754367.jpg"
},
{
"name": "Tamarindo",
"plu": "4448",
"image": "https://server-ifps.accurateig.com/assets/commodities/4448-tamarindo-01_1625669100.jpg"
},
{
"name": "Mandarinas",
"plu": "3524",
"image": "https://server-ifps.accurateig.com/assets/commodities/noble-juicycrunch-productisolated-900x900-rgb-copy_1627662136.png"
},
{
"name": "Taro",
"plu": "4795",
"image": "https://server-ifps.accurateig.com/assets/commodities/4795-taro-root-01_1625753179.jpg"
},
{
"name": "Nabo",
"plu": "4811",
"image": "https://server-ifps.accurateig.com/assets/commodities/4811-turnip-purple-top_1635177577.jpg"
},
{
"name": "Nueces",
"plu": "4943",
"image": "https://server-ifps.accurateig.com/assets/commodities/4943-walnuts_1625757652.JPG"
},
{
"name": "Castañas de agua",
"plu": "4814",
"image": "https://server-ifps.accurateig.com/assets/commodities/4814-waterchestnuts-01_1625753939.jpg"
},
{
"name": "Xpelón",
"plu": "3481",
"image": "https://server-ifps.accurateig.com/assets/commodities/expelon-3481_1625790150.jpg"
},
{
"name": "Yuca",
"plu": "4819",
"image": "https://server-ifps.accurateig.com/assets/commodities/4819-yucca-root-03_1625754507.jpg"
}
]

View File

@@ -1,76 +1,116 @@
import os
import json
import requests
import barcode
import urllib3
import re
from barcode.writer import ImageWriter
from PIL import Image, ImageDraw, ImageFont
import os
import random
from io import BytesIO
# Items to generate
ITEMS = [
{"name": "Plátano", "icon": "🍌"},
{"name": "Manzana", "icon": "🍎"},
{"name": "Tomate", "icon": "🍅"},
{"name": "Lechuga", "icon": "🥬"},
{"name": "Cebolla", "icon": "🧅"},
{"name": "Pan Batido", "icon": "🥖"}
]
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
os.makedirs('keychain_cards', exist_ok=True)
# --- CONFIGURATION ---
JSON_FILE = os.path.join(os.getcwd(), 'frutas.json')
OUTPUT_DIR = os.path.join(os.getcwd(), 'keychain_cards')
IMG_CACHE_DIR = os.path.join(os.getcwd(), 'image_cache')
os.makedirs(OUTPUT_DIR, exist_ok=True)
os.makedirs(IMG_CACHE_DIR, exist_ok=True)
def clean_filename(name):
"""Prevents Windows path errors by stripping illegal characters."""
return re.sub(r'[\\/*?:"<>|]', "_", name)
def get_ean_from_plu(plu):
"""Standard 4-digit PLU to EAN-13 padding."""
return f"000000{str(plu).zfill(4)}00"
def get_cached_image(url, plu):
"""Checks local cache before downloading."""
cache_path = os.path.join(IMG_CACHE_DIR, f"{plu}.jpg")
if os.path.exists(cache_path):
return cache_path
try:
headers = {'User-Agent': 'Mozilla/5.0'}
res = requests.get(url, headers=headers, timeout=10, verify=False)
if res.status_code == 200:
with open(cache_path, 'wb') as f:
f.write(res.content)
return cache_path
except Exception as e:
print(f"❌ Error downloading {plu}: {e}")
return None
def generate_card(item):
name = item['name']
# Generate a private EAN-13 starting with 99
# We need 12 digits (the 13th is a checksum added by the library)
random_digits = ''.join([str(random.randint(0, 9)) for _ in range(10)])
code_str = f"99{random_digits}"
plu = item['plu']
img_url = item['image']
# Generate Barcode Image
EAN = barcode.get_barcode_class('ean13')
ean = EAN(code_str, writer=ImageWriter())
# Use original English name for filename and display
safe_name = clean_filename(name).replace(' ', '_')
final_path = os.path.join(OUTPUT_DIR, f"PLU_{plu}_{safe_name}.png")
# Customizing the output image size
options = {
'module_height': 15.0,
'font_size': 10,
'text_distance': 3.0,
'write_text': True
}
# 1. Skip if already done
if os.path.exists(final_path):
return
# Create the card canvas (300x450 pixels ~ 2.5x3.5 inches)
card = Image.new('RGB', (300, 400), color='white')
# 2. Local Image Fetch
local_img_path = get_cached_image(img_url, plu)
# 3. Canvas Setup
card = Image.new('RGB', (300, 450), color='white')
draw = ImageDraw.Draw(card)
# Draw a border for cutting
draw.rectangle([0, 0, 299, 399], outline="black", width=2)
# Thicker frame as requested (width=3)
draw.rectangle([0, 0, 299, 449], outline="black", width=3)
# Try to add the Emoji/Text (Requires a font that supports emojis, otherwise just text)
if local_img_path:
try:
# If on Linux, try to find a ttf
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 40)
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 25)
img = Image.open(local_img_path).convert("RGB")
w, h = img.size
size = min(w, h)
img = img.crop(((w-size)//2, (h-size)//2, (w+size)//2, (h+size)//2))
img = img.resize((200, 200), Image.Resampling.LANCZOS)
card.paste(img, (50, 40))
except:
font = ImageFont.load_default()
title_font = ImageFont.load_default()
draw.text((150, 140), "[IMG ERROR]", anchor="mm", fill="red")
else:
draw.text((150, 140), "[NOT FOUND]", anchor="mm", fill="red")
# Draw Name and Emoji
draw.text((150, 60), item['icon'], fill="black", font=font, anchor="mm")
draw.text((150, 120), name, fill="black", font=title_font, anchor="mm")
# 4. Text
try:
# Standard Windows font path
f_name = ImageFont.truetype("arialbd.ttf", 22)
f_plu = ImageFont.truetype("arial.ttf", 18)
except:
f_name = f_plu = ImageFont.load_default()
# Save barcode to temp file
barcode_img_path = f"keychain_cards/{code_str}_tmp"
ean.save(barcode_img_path, options=options)
draw.text((150, 260), name.upper(), fill="black", font=f_name, anchor="mm")
draw.text((150, 295), f"PLU: {plu}", fill="#333333", font=f_plu, anchor="mm")
# Paste barcode onto card
b_img = Image.open(f"{barcode_img_path}.png")
b_img = b_img.resize((260, 180)) # Resize to fit card
card.paste(b_img, (20, 180))
# 5. Barcode
EAN = barcode.get_barcode_class('ean13')
ean = EAN(get_ean_from_plu(plu), writer=ImageWriter())
tmp = f"tmp_{plu}"
ean.save(tmp, options={'module_height': 12.0, 'font_size': 10, 'text_distance': 4})
if os.path.exists(f"{tmp}.png"):
b_img = Image.open(f"{tmp}.png")
b_img = b_img.resize((280, 120))
card.paste(b_img, (10, 320))
os.remove(f"{tmp}.png")
# Cleanup and save final
os.remove(f"{barcode_img_path}.png")
final_path = f"keychain_cards/{name.replace(' ', '_')}.png"
card.save(final_path)
print(f"Generated {name}: {ean.get_fullcode()}")
print(f"✅ Card created: {name} ({plu})")
for item in ITEMS:
generate_card(item)
if __name__ == "__main__":
if not os.path.exists(JSON_FILE):
print(f"❌ Missing {JSON_FILE}")
else:
with open(JSON_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
print("\nAll cards generated in 'keychain_cards/' folder.")
print(f"Processing {len(data)} cards...")
for entry in data:
generate_card(entry)
print("\nAll done. Try not to lose your keys.")

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Some files were not shown because too many files have changed in this diff Show More