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:

  1. Bahasa pemrograman yang digunakan adalah Python versi 3.11, pada Sistem Operasi Debian GNU/Linux (saat penulisan artikel ini).
  2. 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.
  3. Semua materi dan dokumentasi proyek ini, akan disimpan dalam Google Drive akun milik Sourabaya In Frame.
  4. 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.
  5. 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.
  6. 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:

templateFront.png, templateFrontModel.png, templateBack.png

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:

202210z000.png, PNG 574x790 574x790+0+0 8-bit sRGB

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:

202210z000-front.jpg <-------------> 202210z000-back.jpg

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