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 fileindex.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:
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.
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:
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