ID Card Behind The Scene
Setelah melewati masa lebih dari 2 tahun semenjak pandemi, seperti yang pernah kita singgung sebelumnya, kali ini kita akan membahas lebih detail tentang proyek member ID card. Artikel kali ini akan bersifat teknis banget, terutama dalam hal programming. Tujuan penulisan artikel ini, terutama memberikan petunjuk teknis secara detail tentang proses pembuatan dan implementasi proyek ID card. Dengan harapan program ini akan bisa berlanjut dan bahkan dikembangkan lebih jauh lagi di masa mendatang oleh generasi Sourabaya In Frame selanjutnya.
Oke, cukup dengan kata pengantarnya, kita akan segera mulai dengan menu utamanya. Beberapa poin penting yang harus digarisbawahi yaitu:
- Bahasa pemrograman yang digunakan adalah Python versi 3.11, pada Sistem Operasi Debian GNU/Linux (saat penulisan artikel ini).
- Untuk pengolah gambar dan menghasilkan desain template ID Card, software yang digunakan adalah Inkscape. Software ini adalah pengolah gambar berbasis vektor, serupa dengan Corel Draw dan Adobe Ilustrator.
- Semua materi dan dokumentasi proyek ini, akan disimpan dalam Google Drive akun milik Sourabaya In Frame.
- Dalam proses pengerjaan proyek ini, secara keseluruhan menggunakan teknologi Open Source yang legal serta bebas biaya lisensi. Disarankan untuk menggunakan Sistem Operasi Linux, karena akan sangat memudahkan proses selanjutnya, dan terutama karena semua tools serta software yang digunakan bisa dengan mudah didapatkan dalam versi Linux-nya.
- Proyek ID Card ini terintegrasi dengan website resmi Sourabaya In Frame. Dengan harapan akan memudahkan proses pengelolaan hingga jangka waktu panjang, ketika member yang terdaftar secara resmi sudah semakin banyak.
- Tujuan jangka panjang dari proyek ini sebetulnya adalah untuk menjalin silaturahmi dan mengembangkan kesempatan berkolaborasi berdasarkan kesamaan minat di bidang fotografi. Dengan adanya data member yang dikelola dengan baik dan berkelanjutan, peluang jejaring antar sesama anggota akan semakin terbuka lebar.
The Folder Structure🔗
Hal pertama yang akan kita ulas adalah tentang struktur folder. Seperti telah disinggung sebelumnya, proyek ini terintegrasi dengan website Sourabaya In Frame, maka struktur folder yang akan kita gunakan akan serupa dengan berikut:
SIF2.0/
├── IDCard
│ ├── assets
│ ├── outputCard
│ └── photoId
└── SIFBlog
Selanjutnya kita akan fokus lebih dulu kepada struktur folder IDCard, sedangkan untuk folder SIFBlog akan kita ulas lebih lanjut saat pembahasan artikel masuk dalam topik integrasi dengan website. Dalam folder IDCard, terdapat 3 subfolder yaitu:
assets
: Folder ini akan berisi template ID Card, file logo SIF, serta berbagai elemen gambar penyusun ID card. File source desain juga bisa diletakkan disini.outputCard
: Folder ini akan menampung hasil jadi file ID card yang nantinya akan di cetak.photoId
: Berisi file foto ID masing - masing pemegang kartu yang sudah di cropping dan disesuaikan dengan format ID card yang akan dicetak.
The Virtual Environment and Dependencies🔗
Ok, kita akan masuk ke topik utama pembahasan artikel ini, yaitu script yang akan kita buat dan kita gunakan untuk menghasilkan file ID Card untuk masing - masing member, berdasarkan data yang kita peroleh dari Google Form yang bisa di isi secara online disini. Namun sebelum itu, kita perlu menyiapkan folder IDCard sebagai folder development python project. Jalankan perintah berikut di command console:
$ cd IDCard
$ python3 -m venv .venv
$ ls -la .venv/
.venv/
├── bin
├── include
├── lib
├── lib64 -> lib
└── pyvenv.cfg
hasil dari command terakhir, struktur folder dibawah .venv/
akan serupa dengan tampilan
diatas. Selanjutnya, untuk bisa menjalankan script yang kita buat, kita membutuhkan beberapa
library software pendukung. Library pendukung ini akan kita masukkan kedalam python virtual
dev environment yang sudah kita siapkan diatas. Caranya, kita akan membuat satu file dengan
nama requirements.txt
di dalam folder IDCard, kemudian copy dan paste kan daftar
library berikut kedalamnya, kemudian simpan.
cachetools==5.3.2
certifi==2023.7.22
charset-normalizer==3.3.2
colorama==0.4.6
DateTime==5.2
et-xmlfile==1.1.0
google-api-core==2.14.0
google-api-python-client==2.107.0
google-auth==2.23.4
google-auth-httplib2==0.1.1
google-auth-oauthlib==1.1.0
googleapis-common-protos==1.61.0
httplib2==0.22.0
idna==3.4
Jinja2==3.1.2
MarkupSafe==2.1.3
numpy==1.26.1
oauthlib==3.2.2
openpyxl==3.1.2
pandas==2.1.2
Pillow==10.0.0
protobuf==4.25.0
pyasn1==0.5.0
pyasn1-modules==0.3.0
pyparsing==3.1.1
pypng==0.20220715.0
python-dateutil==2.8.2
pytz==2023.3.post1
qrcode==7.4.2
requests==2.31.0
requests-oauthlib==1.3.1
rsa==4.9
six==1.16.0
typing_extensions==4.8.0
tzdata==2023.3
uritemplate==4.1.1
urllib3==2.0.7
zope.interface==6.1
Selanjutnya, sebelum memulai sesi coding, kita akan masuk dan mengaktifkan terlebih dahulu
python virtual dev environment dilanjutkan dengan menginstall semua kebutuhan library yang
kita masukkan ke file requirements.txt
tadi. Jalankan command berikut:
$ source .venv/bin/activate
$ pip3 install --upgrade pip
$ pip3 install -r requirements.txt
tunggu beberapa saat sampai semua library berhasil di download dan di install. Terlihat banyak memang, karena sebetulnya dependency library tersebut juga mencakup script lain yang berguna untuk proses integrasi dengan website, yang akan kita bahas lebih lanjut di bagian berikutnya.
The Script (and Assets)🔗
Script yang akan kita buat akan cukup panjang (sekitar 380 baris), oleh karena itu,
akan dibahas tiap bagian secara detail satu per satu. Kita mulai dengan bagian utama
dari script yaitu __main__()
function, yang isinya seperti berikut:
def main(): function🔗
#!/usr/bin/env python3
import argparse
import os
from pathlib import Path
import re
import textwrap
from PIL import Image, ImageDraw, ImageFont, ImageOps
import colorama
import qrcode
# settings and default parameter
scriptPath = os.path.realpath(os.path.dirname(__file__))
defaultPhotoIdFolder = os.path.join(scriptPath, 'photoId/')
defaultOutputFolder = os.path.join(scriptPath, 'outputCard/')
assetsFolder = os.path.join(scriptPath, 'assets/')
baseColor = (220, 220, 220)
wGutter = 0
yGap = 20
colorama.init()
RED = colorama.Fore.RED
RESET = colorama.Fore.RESET
def main():
# processing of arguments and command line options
parser = argparse.ArgumentParser()
parser.add_argument("memberId", help="10 digit alphanumeric member ID")
parser.add_argument("printedName",
help="Printed ID Card name (converted to uppercase)")
parser.add_argument("igAccount",
help="Instagram account ID (without @ sign)")
parser.add_argument("-b", "--back", default="templateBack.png",
help="define ID Card back template file")
parser.add_argument("-f", "--front", default="templateFront.png",
help="define ID Card front template file")
parser.add_argument("-p", "--photo", help="Profile Picture ID file")
parser.add_argument("-o", '--output',
help="Output file/path name (may include full path)")
args = parser.parse_args()
# memberId format must match these regex pattern
idVar = args.memberId
pattern = re.compile("^202[2-5]10[a,b,c,z][0-9]{3}$")
if not pattern.match(idVar):
print(f'{RED}Error: memberId supplied is invalid:{RESET} {idVar}')
exit(1)
nameVar = args.printedName.upper().strip()
pattern = re.compile(r'^[A-Z\s\.]+$')
if not pattern.match(nameVar):
print(f'{RED}Error: printedName format is invalid:{RESET} {nameVar}')
exit(1)
name = nameVar.split(' ')
if len(name) < 2:
fName = name[0]
lName = '*'
else:
fName = name[0]
lName = name[1:]
lName = " ".join(lName)
# igAccount name must match these regex pattern
igVar = args.igAccount
pattern = re.compile(
r'^[a-zA-Z0-9](?:[a-zA-Z0-9]|(?:\.(?!\.))|(?:_(?!_))){0,28}[a-zA-Z0-9_]$')
if not pattern.match(igVar):
print(f'{RED}Error: igAccount name supplied is invalid:{RESET} {igVar}')
exit(1)
if args.photo:
photoIdFile = os.path.join(defaultPhotoIdFolder, args.photo)
else:
photoIdFile = os.path.join(defaultPhotoIdFolder, idVar+".png")
templateFront = args.front if args.front else "templateFront.png"
templateFront = os.path.join(scriptPath, assetsFolder, templateFront)
if not Path(templateFront).is_file():
print(f'{RED}Error: Card front template not found.{RESET} {templateFront}')
exit(1)
templateBack = args.back if args.back else "templateBack.png"
templateBack = os.path.join(scriptPath, assetsFolder, templateBack)
if not Path(templateBack).is_file():
print(f'{RED}Error: Card back template not found.{RESET} {templateBack}')
exit(1)
if args.output:
outputFront = os.path.splitext(args.output)[0]+'-front.png'
outputBack = os.path.splitext(args.output)[0]+'-back.png'
else:
outputFront = idVar+'-front.png'
outputBack = idVar+'-back.png'
path = os.path.join(Path(defaultOutputFolder), args.memberId)
if not os.path.isdir(path):
print(
f'{RED}Notice: output folder "{path}" is missing, creating..{RESET}')
os.mkdir(path)
outputFront = os.path.join(path, outputFront)
outputBack = os.path.join(path, outputBack)
# generate all layer of assets to be composed on card
cardSize = getCardSize(templateFront)
nameImg = generateName(cardSize, fName, lName)
igImg = generateIgAccount(cardSize, igVar)
idImg = generateID(cardSize, idVar)
picImg = putProfilePicAndLogo(cardSize, photoIdFile)
qrCode = generateQRCodeAndUrl(cardSize, idVar)
expDateImg = generateExpiredDate(cardSize)
noteImg = generateNote(cardSize)
archImg = putArchImage(cardSize)
# composing front face card
cardFront = Image.open(templateFront)
cardFront.paste(nameImg, (0, 0), mask=nameImg)
cardFront.paste(igImg, (0, 0), mask=igImg)
cardFront.paste(idImg, (0, 0), mask=idImg)
cardFront.paste(picImg, (0, 0), mask=picImg)
targetF = os.path.basename(outputFront)
targetF = os.path.splitext(targetF)[0]+'.jpg'
targetF = os.path.join(os.path.dirname(outputFront), targetF)
print(f'F: {targetF}')
cardFront.convert("RGB").save(targetF, 'JPEG')
# composing back face card
cardBack = Image.open(templateBack)
cardBack.paste(qrCode, (0, 0), mask=qrCode)
cardBack.paste(expDateImg, (0, 0), mask=expDateImg)
cardBack.paste(noteImg, (0, 0), mask=noteImg)
cardBack.paste(archImg, (0, 0), mask=archImg)
targetB = os.path.basename(outputBack)
targetB = os.path.splitext(targetB)[0]+'.jpg'
targetB = os.path.join(os.path.dirname(outputBack), targetB)
print(f'B: {targetB}')
cardBack.convert("RGB").save(targetB, 'JPEG')
if __name__ == "__main__":
main()
Jika dilihat pada source code script diatas, ada beberapa file asset yang dibutuhkan agar
script tersebut bisa dijalankan. Untuk itu, kita akan kumpulkan file asset tersebut di dalam
folder assets/
yang sudah kita siapkan sejak awal. Sehingga isi dari folder assets/
akan
seperti dibawah ini:
assets/
├── Archipelago.png
├── Fira_Code_SemiBold.ttf
├── Georgia_Bold.ttf
├── Georgia_Italic.ttf
├── IBM_Plex_Mono_Bold.ttf
├── Iosevka_Nerd_Font_Complete_Bold.ttf
├── SIFLogoWhiteTransparent.png
├── templateBack.png
├── templateFront.png
└── templateFrontModel.png
Ada 3 file yang menjadi fokus perhatian kita saat ini yaitu templateFront.png
,
templateBack.png
serta templateFrontModel.png
. Ketiga file tersebut adalah komponen
utama dari desain ID card kita yang sifatnya statis (tidak berubah). Penampakan dari
ketiga file tersebut bisa dilihat berikut ini:
juga yang tidak kalah pentingnya, adalah detail informasi file format dari file template
tersebut. Untuk mendapatkan detail info tentang format file template, kita bisa gunakan
tools identify
. Jalankan perintah berikut di command line Linux:
$ identify templateFront.png
(output..)
Image:
Filename: templateFront.png
Permissions: rw-r--r--
Format: PNG (Portable Network Graphics)
Mime type: image/png
Class: DirectClass
Geometry: 1291x2048+0+0
Resolution: 239.25x239.25
Print size: 5.39603x8.56008
Units: PixelsPerCentimeter
Colorspace: sRGB
Type: TrueColorAlpha
Base type: Undefined
Endianness: Undefined
Depth: 8-bit
...
...
Informasi diatas bisa kita jadikan acuan ketika mendesain ulang ID Card. Informasi yang penting kita perhatikan terutama adalah Format, Geometry, Resolution dan mungkin juga Colorspace serta ColorDepth.
The Details🔗
Script pada pembahasan
sebelumnya sebetulnya belum lengkap. Jika dilihat, definisi berbagai function pada bagian
# generate all layers ...
belum ada. Disini kita akan membahasnya lebih detail, berikut
definisi masing - masing function tersebut:
# get card size from template file
def getCardSize(templateCard):
cardSize = Image.open(templateCard).size
return cardSize
# fungsi untuk mendapatkan ukuran dari file template card, sebagai
# acuan penempatan elemen desain yang lain.
def generateName(): function🔗
# fungsi untuk generate nama yang akan dicetak pada ID card. Antara
# nama depan dan nama belakang menggunakan dua style font dan ukuran
# yang berbeda. Juga penambahan garis separator berwarna merah diatas
# nama ID Card.
def generateName(cardSize, fName, lName):
# variables to set, positioned and style the generated name.
fNameFont = os.path.join(scriptPath, assetsFolder, "Georgia_Bold.ttf")
lNameFont = os.path.join(scriptPath, assetsFolder, "Georgia_Italic.ttf")
if not (Path(fNameFont).is_file() and Path(lNameFont).is_file()):
print(
f'{RED}Required fonts to generate Name NOT found:{RESET}
{fNameFont}, {lNameFont}')
exit(1)
nameFontSize = 104
nameFColor = baseColor
nameLColor = baseColor
separatorColor = (220,35,35)
yName = 1690
yRedLine = 1668
nameGap = 40
font = ImageFont.truetype(fNameFont, size=nameFontSize)
imgFont = Image.new('RGBA', cardSize, (0, 0, 0, 0))
ImageDraw.Draw(imgFont).text((0, 0), fName, font=font, fill=nameFColor)
cropped = imgFont.crop(imgFont.getbbox())
resizedImgF = cropped.resize((cropped.width, int(cropped.height * 1.4)))
font = ImageFont.truetype(lNameFont, size=(nameFontSize-20))
imgFont = Image.new('RGBA', cardSize, (0, 0, 0, 0))
ImageDraw.Draw(imgFont).text((0, 0), lName, font=font, fill=nameLColor)
cropped = imgFont.crop(imgFont.getbbox())
resizedImgL = cropped.resize((cropped.width, int(cropped.height * 1.2)))
nameWidth = resizedImgF.width + resizedImgL.width + nameGap
namePos = (int((cardSize[0] - wGutter - nameWidth) / 2 + wGutter), yName)
imgPad = Image.new('RGBA', cardSize, (0, 0, 0, 0))
imgPad.paste(resizedImgF, namePos, mask=resizedImgF)
lNamePos = (namePos[0] + resizedImgF.width + nameGap, namePos[1])
imgPad.paste(resizedImgL, lNamePos, mask=resizedImgL)
# put red line separator
canvas = Image.new("RGBA", (nameWidth, 10), (0, 0, 0, 0))
ImageDraw.Draw(canvas).rectangle((0,0,nameWidth,10), fill=separatorColor)
redlinePos = (
int((cardSize[0] - canvas.width - wGutter) / 2 + wGutter), yRedLine)
imgPad.paste(canvas, redlinePos, mask=canvas)
return imgPad
def generateIgAccount(): function🔗
# fungsi untuk generate instagram account, dengan menambahkan logo
# instagram di depan Instagram ID yang di setting melalui command
# line argument. Posisi, jenis dan ukuran font yang digunakan, bisa
# di setting melalui variabel dibawah.
def generateIgAccount(cardSize, igName):
igFont = os.path.join(scriptPath, assetsFolder,
"Iosevka_Nerd_Font_Complete_Bold.ttf")
if not (Path(igFont).is_file()):
print(f'{RED}Font to generate IG account NOT found:{RESET} {igFont}')
exit(1)
igFontSize = 70
yIG = 1790
font = ImageFont.truetype(igFont, size=igFontSize)
imgFont = Image.new('RGBA', cardSize, (0, 0, 0, 0))
ImageDraw.Draw(imgFont).text(
(0, 0), " "+igName.lower(), font=font, fill=baseColor)
cropped = imgFont.crop(imgFont.getbbox())
igPos = (int((cardSize[0] - cropped.width -
wGutter) / 2 + wGutter), yIG + yGap)
imgPad = Image.new('RGBA', cardSize, (0, 0, 0, 0))
imgPad.paste(cropped, igPos, mask=cropped)
return imgPad
def generateID(): function🔗
# fungsi untuk generate ID yang akan tercetak menggunakan caps letter
# dan juga menggunakan placeholder box dengan rounded corner untuk
# memperjelas visibility terhadap pola background
def generateID(cardSize, strID):
idFont = os.path.join(scriptPath, assetsFolder, "IBM_Plex_Mono_Bold.ttf")
if not Path(idFont).is_file():
print(f'{RED}Font to print ID code number NOT found:{RESET} {idFont}')
exit(1)
idFontSize = 60
yID = 1598
placeholderFill = (50,50,50,200)
font = ImageFont.truetype(idFont, size=idFontSize)
imgFont = Image.new('RGBA', cardSize, (0, 0, 0, 0))
ImageDraw.Draw(imgFont).text(
(0, 0), "ID:"+strID.upper(), font=font, fill=baseColor)
imgID = imgFont.crop(imgFont.getbbox())
imgID = imgID.resize((int(imgID.width * 1), int(imgID.height * 1.1)))
idPos = (int((cardSize[0] - imgID.width - wGutter) / 2 + wGutter), yID)
# generate ID placeholder
rectDim = (imgID.width + 60, imgID.height + 40)
rectSize = (0,0,rectDim[0], rectDim[1])
canvas = Image.new("RGBA", (rectDim), (0, 0, 0, 0))
ImageDraw.Draw(canvas).rounded_rectangle((rectSize), 20, fill=placeholderFill)
rectPos = (int(idPos[0] - ((rectDim[0] - imgID.width) / 2)), yID - 20)
imgPad = Image.new('RGBA', cardSize, (0, 0, 0, 0))
imgPad.paste(canvas, rectPos, mask=canvas)
imgPad.paste(imgID, idPos, mask=imgID)
return imgPad
def putProfilePicAndLogo(): function🔗
# fungsi ini menggunakan 2 elemen, yaitu file gambar/foto photoId, serta
# logo SIF berwarna putih dan background transparan, yang nantinya berfungsi
# selayaknya watermark diatas file photoId
def putProfilePicAndLogo(cardSize, photoIdFile):
sifLogo = os.path.join(scriptPath, assetsFolder, "SIFLogoWhiteTransparent.png")
if not Path(sifLogo).is_file():
print(f'{RED}Error: SIF Logo file not found:{RESET} {sifLogo}')
exit(1)
if not Path(photoIdFile).is_file():
print(f'{RED}Error: photoId file not found:{RESET} {photoIdFile}')
exit(1)
yPhoto = 590
yLogo = 1110
imgPhoto = Image.open(photoIdFile)
xPhoto = int((cardSize[0] - wGutter - imgPhoto.size[0]) / 2 + wGutter)
photoPos = (xPhoto, yPhoto)
# put SIF logo as watermark
logoScale = 1.32
imgLogo = Image.open(sifLogo)
imgLogo = imgLogo.resize(
(int(imgLogo.width * logoScale), int(imgLogo.height * logoScale)))
xLogo = int((cardSize[0] - wGutter - imgLogo.size[0]) / 2 + wGutter)
logoPos = (xLogo, yLogo)
imgPad = Image.new('RGBA', cardSize, (0, 0, 0, 0))
imgPad.paste(imgPhoto, photoPos, mask=imgPhoto)
imgPad.paste(imgLogo, logoPos, mask=imgLogo)
return imgPad
def generateQRCodeAndUrl(): function🔗
# fungsi ini akan menghasilkan QR Code dari sebuah url yang disusun berdasarkan
# member ID yang di input melalui parameter fungsi idVar. Sedangkan idVar
# diperoleh dari command line arguments saat kita menjalankan script ini.
def generateQRCodeAndUrl(cardSize, idVar):
urlFont = os.path.join(scriptPath, assetsFolder, "Fira_Code_SemiBold.ttf")
if not Path(urlFont).is_file():
print(f'{RED}Font to print QRCode Url NOT found:{RESET} {urlFont}')
exit(1)
qrUrlColor = (230, 230, 230)
urlFontSize = 40
qrImageSize = (680, 680)
yQR = 317
qrValue = "https://sif.my.id/cid/"+idVar
font = ImageFont.truetype(urlFont, size=urlFontSize)
imgFont = Image.new('RGBA', cardSize, (0, 0, 0, 0))
ImageDraw.Draw(imgFont).text((0, 0), qrValue, font=font, fill=qrUrlColor)
cropped = imgFont.crop(imgFont.getbbox())
resizedUrl = cropped.resize((cropped.width, int(cropped.height * 1.2)))
# generate QRCode
qr = qrcode.QRCode( # type: ignore
version=1,
error_correction=qrcode.ERROR_CORRECT_L,
box_size=10, border=0)
qr.add_data(qrValue)
qr.make(fit=True)
qrImg = qr.make_image(fill='black', back_color='white').resize(qrImageSize)
qrPos = (int((cardSize[0] - qrImg.width)/2), yQR)
imgPad = Image.new('RGBA', cardSize, (0, 0, 0, 0))
imgPad.paste(qrImg, qrPos, mask=ImageOps.invert(qrImg))
xUrl = int((cardSize[0] - resizedUrl.width) / 2)
urlPos = (xUrl, yQR + qrImg.height + yGap + 40)
imgPad.paste(resizedUrl, urlPos, mask=resizedUrl)
return imgPad
def generateExpiredDate(): function🔗
# fungsi ini cara kerjanya hampir sama dengan generateId(), karena sama - sama
# menggunakan placeholder box agar content lebih terlihat dengan jelas.
def generateExpiredDate(cardSize):
expString = "valid until: 31/12/2025"
expFont = os.path.join(scriptPath, assetsFolder, "Fira_Code_SemiBold.ttf")
if not Path(expFont).is_file():
print(f'{RED}Font for exp.date NOT found:{RESET} {expFont}')
exit(1)
expFontSize = 44
yExp = 1853
phFill = (30,30,30,200)
font = ImageFont.truetype(expFont, size=expFontSize)
imgFont = Image.new('RGBA', cardSize, (0, 0, 0, 0))
ImageDraw.Draw(imgFont).text((0, 0), expString, font=font, fill=baseColor)
imgExp = imgFont.crop(imgFont.getbbox())
imgExp = imgExp.resize((int(imgExp.width * 1), int(imgExp.height * 1.1)))
expPos = (int((cardSize[0] - imgExp.width - wGutter) / 2 + wGutter), yExp)
# generate exp date placeholder
rectDim = (imgExp.width + 60, imgExp.height + 40)
rectSize = (0,0,rectDim[0], rectDim[1])
canvas = Image.new("RGBA", rectDim, (0, 0, 0, 0))
ImageDraw.Draw(canvas).rounded_rectangle(rectSize, 20, fill=phFill)
rectPos = (int(expPos[0] - ((rectDim[0] - imgExp.width) / 2)), yExp - 20)
imgPad = Image.new('RGBA', cardSize, (0, 0, 0, 0))
imgPad.paste(canvas, rectPos, mask=canvas)
imgPad.paste(imgExp, expPos, mask=imgExp)
return imgPad
def generateNote(): function🔗
# Sama dengan fungsi sebelumnya, hanya saja kali ini content yang di print
# berupa multiline text dan menggunakan center alignment. terdapat beberapa
# penyesuaian yang bisa diatur melalui parameter variabel.
def generateNote(cardSize):
noteString = """Penting: Jika menemukan kartu ini, harap segera menghubungi
sekretariat Sourabaya In Frame. Atau bisa langsung dikembalikan
ke alamat: Jl. Raden Saleh No. 5, Bungur, Medaeng, Sidoarjo"""
noteFont = os.path.join(scriptPath, assetsFolder, "Fira_Code_SemiBold.ttf")
if not Path(noteFont).is_file():
print(f'{RED}Required font for Note NOT found:{RESET} {noteFont}')
exit(1)
noteFontSize = 34
yNote = 1633
phFill = (30,30,30,200)
font = ImageFont.truetype(noteFont, size=noteFontSize)
imgFont = Image.new('RGBA', cardSize, (0, 0, 0, 0))
textWrap = textwrap.fill(noteString, width=63)
ImageDraw.Draw(imgFont).multiline_text((0,0),textWrap, align='center',
font=font, fill=baseColor)
imgNote = imgFont.crop(imgFont.getbbox())
imgNote = imgNote.resize(
(int(imgNote.width * 0.92), int(imgNote.height * 1.5)))
xNote = int((cardSize[0] - imgNote.width - wGutter) / 2 + wGutter)
notePos = (xNote, yNote)
# note placeholder
rectDim = (imgNote.width + 40, imgNote.height + 40)
rectSize = (0,0,rectDim[0], rectDim[1])
canvas = Image.new("RGBA", rectDim, (0, 0, 0, 0))
ImageDraw.Draw(canvas).rounded_rectangle(rectSize, 30, fill=phFill)
rectPos = (int(notePos[0] - 20), int((notePos[1] -
((rectDim[1] - imgNote.height) / 2))))
imgPad = Image.new('RGBA', cardSize, (0, 0, 0, 0))
imgPad.paste(canvas, rectPos, mask=canvas)
imgPad.paste(imgNote, notePos, mask=imgNote)
return imgPad
def putArchImage(): function🔗
# Hanya sebuah fungsi sederhana untuk menempatkan elemen desain berupa
# gambar kepulauan Republik Indonesia (archipelago). File diambil dari
# folder assets.
def putArchImage(cardSize):
archFile = os.path.join(scriptPath, assetsFolder, "Archipelago.png")
if not Path(archFile).is_file():
print(f'{RED}Archipelago image file is NOT found:{RESET} {archFile}')
exit(1)
archImg = Image.open(archFile)
yArch = 1097
xArch = ((cardSize[0] - wGutter - archImg.width) / 2) + wGutter
archPos = (xArch, int(yArch + yGap))
imgPad = Image.new('RGBA', cardSize, (0, 0, 0, 0))
imgPad.paste(archImg, archPos)
return imgPad
Nah, lengkap sudah pembahasan kita tentang script composeCard.py
kali ini. Namun sebelum
melangkah ke topik berikutnya (yang akan dituliskan dalam artikel lain), kita akan coba
menjalankan script yang sudah kita buat diatas, dan melihat hasilnya serta cara kerjanya.
The Execution🔗
Satu hal lagi, sebelum meng-eksekusi script yang kita buat, kita membutuhkan satu file
lagi yaitu file photoId. File ini harus diletakkan dalam foler photoId
serta memiliki
nama file dengan format <memberId>.png
. Lebih jelasnya, penampakan dari file photoId
yang akan kita gunakan sebagai contoh kali ini, adalah seperti berikut:
Untuk keperluan percobaan kali ini, kita akan
menggunakan memberId 202210z000
. Sebelum menjalankan script, pastikan dulu kita telah
berada di dalam folder IDCard
, serta script composeCard.py
juga berada di dalam
folder tersebut. Setelahnya, jalankan command berikut:
(note: perhatikan pada parameter kedua, kita menggunakan tanda petik. Karena input
yang kita masukkan sebagai parameter, memiliki spasi)
$ python3 composeCard.py 202210z000 'john wick' john.wick
(Jika tidak terdapat error, maka output akan serupa berikut..)
F: ../SIF2.0/IDCard/outputCard/202210z000/202210z000-front.jpg
B: ../SIF2.0/IDCard/outputCard/202210z000/202210z000-back.jpg
Kemudian, jika kita perhatikan struktur folder project yang kita miliki sebelumnya, maka akan terlihat seperti berikut, dengan tambahan folder dan file baru sebagai hasil menjalankan script diatas:
SIF2.0/
├── IDCard
│ ├── assets
│ │ ├── Archipelago.png
│ │ ├── Fira_Code_SemiBold.ttf
│ │ ├── Georgia_Bold.ttf
│ │ ├── Georgia_Italic.ttf
│ │ ├── IBM_Plex_Mono_Bold.ttf
│ │ ├── Iosevka_Nerd_Font_Complete_Bold.ttf
│ │ ├── SIFLogoWhiteTransparent.png
│ │ ├── templateBack.png
│ │ ├── templateFront.png
│ │ └── templateFrontModel.png
│ ├── composeCard.py <== (script yang kita buat)
│ ├── outputCard
│ │ └── 202210z000 <== (folder baru berdasarkan memberId)
│ │ ├── 202210z000-back.jpg
│ │ └── 202210z000-front.jpg
│ ├── photoId
│ │ ├── 202210z000.png
│ │ ├── ...
│ │ ├── ...
│ └── requirements.txt
└── SIFBlog
dan inilah hasilnya, file ID Card yang siap untuk dicetak, akan berwujud seperti ini:
Sampai disini berarti script yang kita buat sudah bisa bekerja dengan baik dan siap digunakan untuk men-generate ID Card yang lain berdasarkan data member yang tersimpan dalam database.
by: @adymulianto