ID Card Profile Page

Masih terkait dengan artikel sebelumnya, kali ini kita akan membuat beberapa script pendek untuk menunjang fungsionalitas dari website https://sif.my.id. Diantara script yang akan kita buat yaitu:

  • generateCardSides.py: Script ini berfungsi untuk menggabungkan tampilan depan dan belakang dari file Member ID Card menjadi berjajar secara horizontal. Kemudian juga kita akan menambahkan sebuah fungsi melalui command line option (-x), sehingga memungkinkan gambar hasil penggabungan di convert menjadi grayscale dan ditambahkan tanda stempel EXPIRED berwarna merah.
  • generateProfilePic.py: Untuk script ini, fungsinya adalah menghasilkan gambar foto profil member dengan bingkai sesuai desain seperti yang digunakan pada ID Card. Output dari kedua script ini akan digunakan sebagai pelengkap tampilan pada halaman profil masing - masing member ketika dilakukan verifikasi dengan cara scan pada QR Code yang ada di bagian belakang ID Card.
  • generateCidIndexMd.py: Berfungsi untuk menghasilkan file index.md yang akan berubah menjadi halaman html sesuai dengan Profile Page masing - masing member dengan Member ID nya. Hasil akhir berupa profile page member yang di-generate oleh ketiga script diatas, serupa dengan gambar berikut:
Tampilan profile page masing - masing member

Sebelum memulai, karena artikel ini merupakan kelanjutan dari artikel sebelumnya yaitu ID Card Behind The Scene, maka pastikan terlebih dahulu folder tempat kita bekerja nantinya adalah folder project IDCard/ seperti berikut:

IDCard/
├── assets
   ├── Archipelago.png
   ├── expiredStamp.png
   ├── Fira_Code_SemiBold.ttf
   ├── Georgia_Bold.ttf
   ├── Georgia_Italic.ttf
   ├── IBM_Plex_Mono_Bold.ttf
   ├── Iosevka_Nerd_Font_Complete_Bold.ttf
   ├── RegistrasiMember.xlsx
   ├── SIFLogoWhiteTransparent.png
   ├── templateBack.png
   ├── templateFrontModel.png
   ├── templateFront.png
   ├── templateIndexMd.md
   └── templatePhotoCV.png
├── composeCard.py
├── generateCardSides.py
├── generateCidIndexMd.py
├── generateProfilePic.py
├── generateProfile.py
├── outputCard
   └── 202210z000
       ├── 202210z000-back.jpg
       └── 202210z000-front.jpg
├── photoId
...
...
├── requirements.txt

Untuk script yang akan kita buat, nantinya berada dalam folder IDCard/ tersebut, sesuai dengan skema folder seperti diatas. Sebelum memulai coding, ada baiknya kita aktifkan dulu python virtual environment, agar semua library dan requirements telah aktif dan dikenali. Jalankan command berikut:

$ source .venv/bin/activate

maka python virtual environment telah aktif, dan kita bisa mulai bekerja.

generateCardSides.py script🔗

Baiklah, tanpa perlu berpanjang lebar, berikut adalah isi dari script pertama yaitu generateCardSides.py. Pada script ini, ada 2 parameter yang wajib ada yaitu img1 dan img2, sedangkan -o atau --output serta -x atau --expired sifatnya optional. Option -x digunakan jika ingin menghasilkan gambar format grayscale dengan stamp Expired berwarna merah. Untuk menunjukkan bahwa ID Card yang bersangkutan telah kadaluarsa dan perlu diperbaharui.

#!/usr/bin/env python3

from PIL import Image, ImageEnhance
from pathlib import Path
import argparse, colorama, os

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("img1", help="Front face card image")
    parser.add_argument("img2", help="Back face card image")
    parser.add_argument("-o", '--output', default="cardMerge.jpg",
        help="Output file name include path/folder.")
    parser.add_argument("-x", '--expired', action="store_true",
        help="Turn the output image as grayscale and put 'Expired' sign")
    args = parser.parse_args()
    scriptPath = os.path.realpath(os.path.dirname(__file__))
    assetsFolder = os.path.join(scriptPath, 'assets/')
    expImg = os.path.join(assetsFolder, 'expiredStamp.png')

    colorama.init()
    RED = colorama.Fore.RED
    RESET = colorama.Fore.RESET

    try:
        file1 = Path(args.img1).resolve()
        if not file1.is_file():
            raise ValueError(f"{RED}Error: {str(file1)} not found.{RESET}")
    except ValueError as e:
        print(f"{__file__}: {e}")
        exit(1)

    try:
        file2 = Path(args.img2).resolve()
        if not file2.is_file():
            raise ValueError(f"{RED}Error: {str(file2)} not found.{RESET}")
    except ValueError as e:
        print(f"{__file__}: {e}")
        exit(1)

    path = Path(args.output)
    dirName, fileName = os.path.split(path)
    _, fExt = os.path.splitext(fileName)
    if not fExt:
        print(f'{__file__}: {RED}Error: no output file extension specified{RESET}')
        exit(1)
    if dirName and not os.path.isdir(dirName):
        print(f'{__file__}: {RED}Error: output folder {dirName} is missing..{RESET}')
        exit(1)

    try:
        img1 = Image.open(file1)
        img2 = Image.open(file2)
    except IOError:
        print(f"{__file__}: {RED}Error opening image file(s).{RESET}")
        exit(1)

    dst = Image.new('RGB', (img1.width + img2.width + 20, img1.height), "white")
    dst.paste(img1, (0,0))
    dst.paste(img2, (img1.width + 20, 0))
    width, height = int(dst.width/2), int(dst.height/2)
    dst = dst.resize((width, height))

    if args.expired:
        dst = dst.convert('L')
        try:
            expSign = Image.open(expImg)
        except IOError:
            print(f"{__file__}: {RED}Error opening Expired stamp image.{RESET}")
            exit(1)
        brightness = ImageEnhance.Brightness(dst)
        contrast = ImageEnhance.Contrast(dst)
        dst = contrast.enhance(0.8)
        dst = brightness.enhance(1.5)
        xRatio = expSign.height / expSign.width
        xWidth, xHeight = int(dst.width * 0.8), int(dst.height * 0.8 * xRatio)
        expSign = expSign.resize((xWidth, xHeight))
        expPosition = (
            int((dst.width - expSign.width) / 2),
            int((dst.height - expSign.height) / 2)
        )
        dst = dst.convert('RGB')
        dst.paste(expSign, expPosition, mask=expSign)

    try:
        print(f'output to file: {os.path.join(dirName, fileName)}')
        dst.save(os.path.join(dirName, fileName))
    except IOError:
        print(f"{__file__}: {RED}Error saving output image.{RESET}")
        exit(1)


if __name__ == "__main__":
    main()

Jika kita coba menjalankan script tersebut, akan seperti berikut:

$ ./generateCardSides.py outputCard/202210z000/202210z000-front.jpg \
  outputCard/202210z000/202210z000-back.jpg
output to file: cardMerge.jpg <== (keterangan nama file output)

secara default, jika tanpa memberikan option -o, maka file output dari hasil menjalankan script diatas adalah cardMerge.jpg yang berada di folder yang sama tempat kita menjalankan script tersebut (dalam hal ini di folder IDCard/).

Dan berikut adalah contoh jika script generateCardSides.py dijalankan dengan dan tanpa menggunakan -x option argument.

Tampilan hasil normal tanpa `-x` options
Tampilan grayscale dengan stamp Expired karena `-x` options

generateProfilePic.py script🔗

Ok, sekarang kita akan lanjut membahas script kedua yaitu generateProfilePic.py. Script ini berfungsi untuk menghasilkan foto profile dengan bingkai yang di desain selaras dengan desain ID Card. Penambahan watermark berupa logo SIF, juga sekaligus berfungsi agar foto profil yang bersangkutan tidak mudah untuk diambil dan digunakan untuk tujuan lain oleh pihak yang tidak bertanggung jawab. Adapun isi dari script tersebut, bisa dilihat dibawah ini:

#!/usr/bin/env python3

from PIL import Image
from pathlib import Path
import argparse
import colorama
import re
import os


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("memberId",
                        help="10 digit alphanumeric member ID")
    parser.add_argument("-t", "--template",
                        default="templatePhotoCV.png",
                        help="Pofile Photo frame template")
    parser.add_argument("-p", "--photo",
                        help="define Profile Picture ID file")
    parser.add_argument("-o", '--output',
                        help="Output file name with full path and file extension. \
                        Default to [ID code number].png")
    args = parser.parse_args()

    colorama.init()
    RED = colorama.Fore.RED
    RESET = colorama.Fore.RESET

    # default settings and parameter
    scriptPath = os.path.realpath(os.path.dirname(__file__))
    assetsFolder = os.path.join(scriptPath, 'assets/')
    photoIdFolder = os.path.join(scriptPath, "photoId/")
    yPos = 275
    sifLogo = os.path.join(assetsFolder, "SIFLogoWhiteTransparent.png")
    logoScale = 1.32
    wGutter = 0

    idVar = args.memberId
    pattern = re.compile("^202[2-4]10[a,b,c,z][0-9]{3}$")
    if not pattern.match(idVar):
        print(f'{RED}Error: memberId {idVar} supplied is not valid')
        exit(1)

    if args.photo:
        photoIdFile = os.path.join(photoIdFolder, args.photo)
        path = Path(photoIdFile)
        if not path.is_file():
            print(f'{RED}Error: {photoIdFile} not found, please check..{RESET}')
            exit(1)
    else:
        photoIdFile = os.path.join(photoIdFolder, idVar+".png")

    if args.template:
        templateFront = os.path.join(assetsFolder, args.template)
        path = Path(templateFront)
        if not path.is_file():
            print(f'{RED}Error: {templateFront} not found, please check..{RESET}')
            exit(1)
    else:
        templateFront = os.path.join(assetsFolder, "templatePhotoCV.png")

    if args.output:
        path = Path(args.output)
        dirName, fileName = os.path.split(path)
        _, fExt = os.path.splitext(fileName)
        if not fExt:
            print(f'{RED}Error: no output file extension specified{RESET}')
            exit(1)
        if dirName and (not os.path.isdir(dirName)):
            print(
                f'{RED}Warning: output directory {dirName} not found. creating...{RESET}')
            os.mkdir(dirName)
        outputFile = os.path.join(dirName, fileName)
    else:
        outputFile = idVar+'.png'

    # ------------------------- Place Photo ID -------------------------------
    photoIdImg = Image.open(photoIdFile)
    templateImg = Image.open(templateFront)
    widthTemplate, heightTemplate = templateImg.size
    widthImg, _ = photoIdImg.size
    templateImg.paste(
        photoIdImg, (int((widthTemplate - widthImg)/2), int(yPos)), photoIdImg)

    # ------------------------- Place SIF Logo -------------------------------
    imgLogo = Image.open(sifLogo)
    imgLogo = imgLogo.resize(
        (int(imgLogo.width * logoScale), int(imgLogo.height * logoScale)))
    xLogo = (widthTemplate - wGutter - imgLogo.size[0]) / 2 + wGutter
    yLogo = yPos + 520
    logoPos = (int(xLogo), int(yLogo))
    templateImg.paste(imgLogo, logoPos, mask=imgLogo)

    templateImg = templateImg.resize(
        (int(widthTemplate * 0.2), int(heightTemplate * 0.2)))
    templateImg.save(outputFile)


if __name__ == "__main__":
    main()

Baiklah, rasanya tidak banyak yang perlu dijelaskan dari script ini. Selain bahwa template photo yang secara default berada di folder assets/. Dan script ini hanya memiliki satu parameter wajib yaitu nomor ID Card. Berdasarkan kode ID Card tersebut, script ini akan mengambil file foto member dari folder photoId/ dan mencari file foto dengan nama <IDCard Number>.png untuk kemudian digabungkan dengan template (bingkai foto) serta logo SIF sebagai watermark.

generateCidIndexMd.py script🔗

Untuk menampilkan halaman Profile Page seperti ini, tentunya tidak hanya dibutuhkan gambar saja. Namun dengan sebuah SSG semacam Zola, kita membutuhkan juga sebuah dokumen markdown yang nantinya akan otomatis diterjemahkan menjadi halaman HTML oleh Zola. File markdown ini harus disimpan dengan nama index.md, serta memiliki format tertentu agar bisa dikenali dan diterjemahkan oleh Zola.

Nah, itulah tugas dan fungsi dari script kita yang ketiga ini generateCidIndexMd.py. Yang isinya seperti berikut:

#!/usr/bin/env python

from jinja2 import Template
import pandas as pd
import string
import re
import os
import argparse
from datetime import datetime as dt
import colorama

scriptName = os.path.basename(__file__)
scriptPath = os.path.realpath(os.path.dirname(scriptName))
assetsFolder = os.path.join(scriptPath, 'assets/')
cidFolder = os.path.realpath(os.path.join(
    scriptPath, "../SIFBlog/content/cid/"))
dataFile = os.path.join(assetsFolder, 'RegistrasiMember.xlsx')
templateF = os.path.join(assetsFolder, 'templateIndexMd.md')
validUntil = 'December, 2025'

colorama.init()
RED = colorama.Fore.RED
RESET = colorama.Fore.RESET

colDataSet = [
    'fullname',
    'residence',
    'birthday',
    'gender',
    'facebook',
    'instagram',
    'email',
    'id',
    'printedname',
    'status'
]

df = pd.read_excel(dataFile, usecols=[2, 3, 4, 5, 9, 10, 11, 15, 16, 17],
                   skiprows=1, header=None, names=colDataSet)


def checkIsValidId(memberId):
    global dataSet
    dfId = df[df["id"] == memberId.upper().strip()]

    if not dfId.empty:
        # populate dataSet global variable
        dataSet = dfId.iloc[0]
        return (True)
    else:
        return (False)


def genIndexMd(row):
    memDict = {
        'date': dt.now().strftime("%Y-%m-%d"),
        'fullname': string.capwords(row.printedname.lower()),
        'id': row.id,
        'idlower': row.id.lower(),
        'gender': 'Female' if row.gender == 'WANITA' else 'Male',
        'email': '-' if row.email == 'nan' else
        str(re.sub('@', '[at]', row.email)).lower(),
        'instagram': row.instagram.lower(),
        'birthday': dt.strptime(str(row.birthday),
                                "%Y-%m-%d %H:%M:%S").strftime("%B, %Y"),
        'residence': row.residence.split(',')[-1].strip(),
        'validity': validUntil
    }

    with open(templateF, 'r') as templateFile:
        mdTemplate = templateFile.read()
    template = Template(mdTemplate)
    output = template.render(mb=memDict)
    return (output)


def main():
    parser = argparse.ArgumentParser(scriptName)
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("-a", "--all", action="store_true",
                       help="Generate all profiles at once")
    group.add_argument("--id", help="10 digit alphanumeric member ID")
    args = parser.parse_args()

    if not os.path.isfile(dataFile):
        print(f'{RED}Error: required data file: {dataFile} not found{RESET}')
        exit(1)

    if args.id:
        # check that supplied id is valid and found in dataSet
        if not checkIsValidId(args.id):
            print(
                f'memberId: ({args.id}) is not found nor valid, please check.')
            exit(1)
        # generate and write output to index.md for SIFBlog cid member page
        print(f"Generate index.md for: {args.id.lower()}")
        output = genIndexMd(dataSet)
        cidFolderId = os.path.join(cidFolder, dataSet.id.lower())
        if not os.path.isdir(cidFolderId):
            os.mkdir(cidFolderId)
        f = open(os.path.join(cidFolderId, 'index.md'), 'w')
        f.write(output)
        f.close()
    else:
        iter = 1
        for _, row in df.iterrows():
            output = genIndexMd(row)
            targetFolderId = os.path.join(cidFolder, row.id.lower())
            if not os.path.isdir(targetFolderId):
                os.mkdir(targetFolderId)
            f = open(os.path.join(targetFolderId, 'index.md'), 'w')
            print(
                f'{iter}.\tCreate index.md for: ({row.id.lower()}) {targetFolderId}')
            f.write(output)
            f.close()
            iter += 1


if __name__ == "__main__":
    main()

Perhatikan bahwa untuk menjalankan script generateCidIndexMd.py diatas, kita membutuhkan dua file asset yaitu templateIndexMd.md yang merupakan file template untuk men-generate file index.md, serta file RegistrasiMember.xlsx yang berisikan data Member sesuai yang telah melakukan pendaftaran melalui form online bit.ly/sif-form. Perlu diingat, bahwa kedua file tersebut harus diletakkan di dalam folder assets/.

Untuk lebih memahami cara kerja script ini dengan file templateIndexMd.md, berikut isi dari file template tersebut:

Isi file `templateIndexMd.md`

Perhatikan bahwa semua placeholder (posisi variable) yang berada dalam tanda {{}} akan otomatis digantikan dengan data yang dihasilkan oleh script. Serta hasilnya berupa file index.md akan berada di struktur folder dari website SIFBlog/, pada profile page masing - masing member. File inilah yang akan muncul ketika kita melakukan scan QRCode pada halaman belakang ID Card, yang akan menuju ke alamat seperti https://sif.my.id/cid/202210z000. dengan bagian terakhir adalah ID sesuai masing - masing member.

Dengan artikel ini, menjelaskan keterkaitan antara project ID Card dengan Website Sourabaya In Frame. Dalam artikel berikutnya, kita akan lebih fokus dengan pembahasan mengenai proses pembangunan Website itu sendiri. Disana nantinya akan dibahas lebih detail teknologi yang digunakan serta prosedur umum dalam pengembangan konten serta artikel yang menjadi fungsi utama dibuatnya website tersebut. Untuk saat ini, alamat website resmi Sourabaya In Frame adalah https://sif.my.id. Silakan dikunjungi dan sekiranya ada artikel maupun konten yang menarik ataupun saran serta kritik mengenai konten yang ada, bisa langsung inbox atau DM ke admin.

Buat SIFers semua, yuk! ikut berpartisipasi menambah konten dan ide yang menarik seputar fotografi atau bahkan hal umum lainnya yang bermanfaat..

by: @adymulianto