Chuyển đổi ảnh sang ASCII art cực ngầu bằng Javascript thuần

Dạo gần đây tự nhiên rộ lên cái trend convert ảnh sang ASCII art (ảnh dưới dạng các ký tự ASCII). Sau khi tìm hiểu thì mình thấy cách phổ biến nhất mà mọi người thường làm là lên google search "convert image to ASCII art" rồi upload ảnh lên, sau đó copy các ký tự ASCII về rồi in ra hoặc dùng một lib nào đó. Những cách đó cũng được thôi, nhưng bạn sẽ không thể custom cũng như là không hiểu các tool đó họ làm như thế nào.

Bạn nào nôn nóng có thể xem trước demo hoặc Github Repo

Trong bài viết này mình sẽ hướng dẫn các bạn convert từ ảnh sang ASCII art chỉ bằng javascript và không dùng bất kỳ thư viện nào cả. Ok bắt đầu nhé.

Thuật toán chuyển ảnh sang ASCII art

Có một bài post thú vị các bạn có thể xem How do ASCII art image conversion algorithms work?

Chuyển sang ASCII art cơ bản có 2 bước:

  1. Chuyển ảnh màu sang ảnh trắng đen (gray colors)
  2. Map từng pixel sang các ký tự dựa trên giá trị grayscale

Ví dụ, @ thì tối màu hơn +, cũng tối màu hơn ..

Trong bài viết này, mình sẽ triển khai thuật toán này bằng JS thuần và chạy trên trình duyệt.

Upload ảnh sang Canvas

hot girl nhật
Niiya Mayu cho bác nào muốn hỏi

Bước đầu tiên chúng ta cần cho phép người dùng upload ảnh, vì thế ta cần một input file. Sau đó mình sẽ phân tích ảnh sang từng pixel, mình sẽ dùng một canvas. Dưới đây là HTML mà mình sử dụng.

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Ascii Art Converter</title>
</head>
<body>
<h1>Ascii Art Converter</h1>
<input type="file" name="picture" />
<canvas id="preview"></canvas>
</body>
</html>

Tại bước này, mình có thể upload một ảnh lên input nhưng chưa có gì xảy ra cả. Đó là bởi vì mình cần chuyển file ảnh đó sang canvas element. Mình sẽ sử dụng FileReader API:

js
const canvas = document.getElementById('preview')
const fileInput = document.querySelector('input[type="file"]')
const context = canvas.getContext('2d')
fileInput.onchange = e => {
// chỉ xử lý upload 1 file
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = event => {
const image = new Image()
image.onload = () => {
canvas.width = image.width
canvas.height = image.height
context.drawImage(image, 0, 0)
}
image.src = event.target.result
}
reader.readAsDataURL(file)
}

Khi input thay đổi, mình sẽ tạo một FileReader object mới, điều này sẽ giúp mình đọc file và chuyển vào canvas. Mình đã đặt size canvas bằng với size ảnh upload để tránh bị giảm chất lượng. 2 tham số cuối của drawImage xác định nơi đặt ảnh trong canvas. Trong trường hợp này, mình muốn vẽ một bức 'ảnh' từ vị trí góc trên cùng bên trái. (tọa độ [0,0]).

Một khi mình đã nhúng đoạn script này vào trang HTML, mình có thể upload một bức ảnh bất kì và nó sẽ hiển thị trong canvas element.

hot girl nhật

Chuyển ảnh sang ảnh trắng đen Gray Colors

Bây giờ bức ảnh đã được upload, mình cần chuyển nó sang ảnh trắng đen. Màu của mỗi pixel được tạo từ 3 màu cơ bản: red, green, blue như trong giá trị hexadicimal color css (#RRGGBB).

Một cách đơn giản để tính được thang màu xám tương ứng là lấy trung bình 3 giá trị này. Tuy nhiên, mắt con người chúng ta thì không nhạy cảm như nhau với 3 màu này. Ví dụ, mắt chúng ta rất nhạy cảm với màu green, trong khi blue thì sẽ hời hợt 1 chút. Do đó, chúng ta cần cân nhắc sử dụng các màu với các tỉ lệ khác nhau. Sau khi nghiên cứu chi tiết Grayscale Wikipedia Page, mình quyết định tính giá trị grayscale dựa trên công thức dưới đây:

bash
GrayScale = 0.21 R + 0.72 G + 0.07 B

Vì thế mình cần lặp qua mỗi pixel của bức ảnh và thay thế nó bằng giá trị grayscale. canvas API cung cấp function getImageData để chúng ta phân tách từng pixel của bức ảnh.

js
const toGrayScale = (r, g, b) => 0.21 * r + 0.72 * g + 0.07 * b
const convertToGrayScales = (context, width, height) => {
// Lấy data image từ canvas tại tọa độ [0,0]
// với chiều rộng và chiều cao
const imageData = context.getImageData(0, 0, width, height)
const grayScales = []
for (let i = 0; i < imageData.data.length; i += 4) {
const r = imageData.data[i]
const g = imageData.data[i + 1]
const b = imageData.data[i + 2]
const grayScale = toGrayScale(r, g, b)
imageData.data[i] = imageData.data[i + 1] = imageData.data[i + 2] = grayScale
grayScales.push(grayScale)
}
// Vẽ một bức ảnh dựa trên image data có sẵn,
// bắt đầu tại tọa độ [0,0]
context.putImageData(imageData, 0, 0)
return grayScales
}

getImageData sẽ cho ra một object chứa thuộc tính data là một Uint8ClampedArray. Data này là một mảng một chiều chứa các giá trị red, green, blue, alpha (RGBA) theo thứ tự, với các số nguyên có giá trị từ 0 - 255. Vì thế mình lặp qua mảng bằng cách tăng lên 4 mỗi lần lặp, lấy giá trị RGB từ 3 phần tử đầu tiên, tính toán tỉ lệ gray và sau đó tiếp tục cho đến hết.

Lưu ý là RGBA khác RGBa nha. Với RGBA thì A là alpha đại diện cho độ trong suốt có giá trị là số nguyên từ 0 - 255. Còn RGBa được sử dụng phổ biến trong CSS với a cũng là alpha nhưng giá trị là số thập phân từ 0 - 1. Nghe có vẻ RGBa có thể biểu thị được alpha nhiều hơn vì không giới hạn về mặt lý thuyết nhưng điều này về mặt phần cứng thì không phải vậy. Có sự khác biệt nào giữa alpha 0.5000000.500001? Mình nghĩ là không. Mình nghĩ rằng từ 0.0 - 1.0 thì CSS sẽ chuyển sang 0 - 255 dựa trên giá trị nào gần hơn.

Trong đoạn code này, mình đã thay đổi image data gốc. Nghĩa là function convertToGrayScales() không được thuần khiết (impure).

Sau đó, mình chỉ cần gọi convertToGrayScales tại dòng cuối cùng của image.onload listener. Và bây giờ ảnh được upload sẽ chuyển sang màu xám.

hot girl nhật

Map các Pixel sang các ký tự tương ứng

Để hiển thị ảnh sử dụng chuỗi các ký tự ASCII, bước tiếp theo chúng ta cần làm là chọn mỗi ký tự cho mỗi pixel ảnh. Một số ký tự thì tối màu hơn số còn lại. Ví dụ @ thì tối hơn ., bởi vì nó chiếm nhiều không gian màn hình hơn. Chuỗi ký tự dưới đây thường được dùng để biểu diễn các cấp độ của màu xám.

69 cấp độ màu xám

sh
grayRamp1 = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "

hoặc 10 cấp độ màu xám

sh
grayRamp2 = "@%#*+=-:. "

Càng nhiều cấp độ màu thì ASCII art của chúng ta sẽ hiển thị được nhiều sắc thái màu hơn (không liên quan độ nét nha).

Map một giá trị gray scale với ký tự tương đương.

js
const grayRamp = '$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,"^`\'. '
const rampLength = grayRamp.length
// Giá trị grayScale là một số nguyên từ 0 (black) đến 255 (white)
const getCharacterForGrayScale = grayScale => grayRamp[Math.ceil(((rampLength - 1) * grayScale) / 255)]

Bây giờ chúng ta cùng chuyển ảnh sang các ký tự nào:

js
const asciiImage = document.querySelector('pre#ascii')
const drawAscii = grayScales => {
const ascii = grayScales.reduce((asciiImage, grayScale) => {
return asciiImage + getCharacterForGrayScale(grayScale)
}, '')
asciiImage.textContent = ascii
}

Mình dùng thẻ <pre> để giữ đúng tỉ lệ của bức ảnh kết hợp với đó là dùng monospaced font.

Gọi hàm drawAscii ở cuối cùng của image.onload callback, mình nhận được kết quả như dưới đây:

hot girl nhật

Thoạt nhìn, nó có vẻ không hoạt động. Nhưng nếu bạn scroll ngang, bạn để ý thấy một số chuỗi xuất hiện trên màng hình. Bức ảnh của chúng ta có vẻ hiển thị trên 1 dòng. Thực sự thì tất cả các pixel được biểu diễn trên 1 mảng 1 chiều. Vậy nên mình cần cho nó xuống dòng dựa trên giá trị chiều rộng pixel bức ảnh width.

js
const drawAscii = (grayScales, width) => {
const ascii = grayScales.reduce((asciiImage, grayScale, index) => {
let nextChars = getCharacterForGrayScale(grayScale)
if ((index + 1) % width === 0) {
nextChars += '\n'
}
return asciiImage + nextChars
}, '')
asciiImage.textContent = ascii
}

Kết quả bây giờ tốt hơn nhiều rồi, ngoại trừ việc nó quá chi tiết (tức là độ phân giải quá cao)

hot girl nhật

Các ký tự ASCII đại diện cho ảnh thực sự khổng lồ. Mình đã map mỗi pixel cho một ký tự. Thử vẽ một bức tranh nhỏ với 10x10 pixel sẽ tốn 10 dòng với 10 ký tự. Chúng quá lớn.

Mình có thể giữ bức tranh bằng ký tự khổng lồ này và giảm font-size lại cho bằng bức ảnh đã được upload. Nhưng mà điều này không tối ưu lắm, nó làm trình duyệt render hơi khá lâu và nếu như bạn chia sẽ ảnh ASCII này cho người khác bằng email thì có thể bị giới hạn ký tự.

Làm giảm độ phân giải ảnh ASCII

Phương thức drawImage của canvas còn nhận vào 2 tham số nữa đó là output width và height. Vì thế chúng ta có thể điều chỉnh lại kích thước ảnh đầu ra. Mình không rõ tiến trình xử lý ảnh phía dưới nó xử lý như thế nào để giảm độ phân giải nhưng mình đoán là nó là subsampling process.

Ok, cùng giảm độ phân giải ảnh nào.

js
const MAXIMUM_WIDTH = 80
const MAXIMUM_HEIGHT = 80
const clampDimensions = (width, height) => {
if (height > MAXIMUM_HEIGHT) {
const reducedWidth = Math.floor((width * MAXIMUM_HEIGHT) / height)
return [reducedWidth, MAXIMUM_HEIGHT]
}
if (width > MAXIMUM_WIDTH) {
const reducedHeight = Math.floor((height * MAXIMUM_WIDTH) / width)
return [MAXIMUM_WIDTH, reducedHeight]
}
return [width, height]
}

Mình tập trung vào chiều cao trước tiên. Thực sự để một bức ảnh được đánh giá cao thì người dùng không cần phải cuộn thanh scrollbar. Cũng lưu ý là mình giữ nguyên tỉ lệ ảnh để tránh ảnh bị biến dạng.

Bây giờ mình có thể cập nhật image.onload để sử dụng giá trị widthheight mới.

js
image.onload = () => {
const [width, height] = clampDimensions(image.width, image.height)
canvas.width = width
canvas.height = height
context.drawImage(image, 0, 0, width, height)
const grayScales = convertToGrayScales(context, width, height)
drawAscii(grayScales, width)
}

Bây giờ, mình upload bức ảnh cô nàng hot girl của mình, và đây là kết quả:

text
JvxZZcUbaZYQ0pUvxx|rv{+_?<>~-|(UCp0wwpkkk0XOvJC0wbhkdZCQZZpk#W8
Qu[|Z(_ChahmXpLXJuzU]/?[>>?[]/uUCCpdddbhkpcCLmZUZwqwmZpqZ0Obo#W
OZc)Or!1ObaqXCCQL0puYn1{?~}1(|cUQ0doaahkbbmUL0LYZwwqphkq0LZdhoo
CLQuCviitmaqvcmpZmOXqXn[(1[|rjc0Odqk*ahbbhkCzYYfXOdk*#qZCJmbdZm
zObmqJ]?[{xmmmbh0oqhZxzxt1xxjczJLOdko#hhaahqUnvXCZdk*#dbqZmb0QO
rQoooL({+-/CpmkbkkwaqZuxj{YcuUULZmpkhMMaaaopJYcUmppwkomOwZwphao
UQwkaqJLXuzXQd*hhkhoppCnr|JUYQJJqmdohW&#haahmCZZdbmZbhZ0wmpk#&8
LQ0qd0(cZXtfuk*kododbq0rntJCXC0Qqqqbk#MW#haoqCpbh#hwk*bpdqpb#W8
ZQuCmZvJpwQ0O#k##aMdbmQvn)XLr00JOdZoo*M&WaaohpaaaMaqaopqq0OpoM%
cjtjUZwZmpZvO&ah#*#hkwOrz{L0jZLmwkwh**M&8#oo*ko###dmkd0LQLCOko&
/1cnQOLOpkwtmM#o*WMoawwXJ[Zdvzpdpddk*oM&8Wh#*akbbkOZpZXvYJZZqh#
jjLp0CObaOZwo*#Ma&MahqqYL]mkvvqOpbwhao*8%&##oamOOOJOZOYXQZZL0b*
rt|fxYZkkmpk#*oWh8#oamqQQ{rkcnXqdhhhaoWM8%M*MobJYYYUcczCmZCQqba
Y|}{cwqpdbhd#W##*MM#*mqOQ})kL{r0kbohhkoM8B%#**mUvvuxYJLOZO0pbdk
Q()CqL0pbhhb#W##&8&M*qq0Q}]Uo{}Um#aph#*o#%%M*MqXzu/rCJUCL0Zbhbb
QcXk0nYwpwdhWW*&8%&M*qqUQ1?va|[(Ckapa*MMM8B8M*awLzJCJcvJ0L0qkkk
kbaar(cQmQmbWW##%B8MkZQxL{[rkX[[1QddwmqdhMB&W#hqXx00UYXUJULZphk
M&&0)tJZppZbWWMM%%&#dqmLpYcuZk|?)nzJObqCmo8%MWabpZQJXXYUQQLLmkb
#W#QzZr)uQZbMWMWB8&opmmQ0UXczZ1[|rXh*qOqJQ#B8&*#omLUuYYJmZLObah
ookdOQt]1JCZ#&W&B%&mcrrXZqJcx/[[(xzZwcnJxnbB%&W#oputvUzzUQ0mdkh
adOmUX(i-CcLkWMWB%MCnrYOqoYzx/]_1fxx/)(1}(mB%&&akbLjjXcuvYY0kod
aakqv{>;-uunp&WWBB#XtrLYuXr|t)->-}1(1}?~~]LBB&8akbdUxvvvcXJma#q
khk0t~><+[]-ua&&BBMz1[(rt)1({]+;l~___<il>]CBB88apZOnjuvuuzXLbhh
bbpc??[))-_?tkWW%B&J}__?]]--_+>,"!>>>lII>}O@B8&*qUCn|nvnnvzUqdd
dJvJ1]{1}_~_}JoM%@%m{~>>>>>><<i,^I><iIIl<)k@@%8#bJrttjvnnuzU0wO
mn1Yt-|[]->+[X**%BBWt+>i!l!l>+i"`:<_<i!i+t#@B8M#bqYrvtvcvzJCLmq
Jc/}|??()?-}|1O*8%B%L[~>!lli+_i^'^l~+~<~]uW@@88Mddm0YzXczJOZZZw
O0U[[]?jcj{1txm*W%%%oj?~>ii<__<:"I~i!<~-1Q%BB8%MdwCYYccvzQmOOmk
U00r]?]c00CJLCmoM88%801-~<<~~[}}_?)iIi~]/p%B%&%WhqOQQJcjxJwwZZb
|)}}_-]/fXuLOUpMo&888kr}-+~>!_|f/1+Il+-}z8B@%&8&M*apZZZYrUOqwwZ
<!;I~}){-/j0bbk#oW8888O(?_<>!l+{)1{{)f|fp%B@%&8&#hbpOZqbOOmZZmZ
!I!+1/|}/YLqqhd*#M&8&8MY1-+_--fzuxzj_~10WBBB%W%&WkkbmZwbdwZwq0m
[}1{++1]cpbwUc0o#M&&88&WJ(]1rzvvvfI"`^;r*%@%&M8WMMhhbddbppqkkpd
-f/_ll!_xLmLvfmao#M8W8888Zxfffnc(>i!>iII]aB8W#8WM&ahbdpppbdkhbb
?x?I~{1|})xuUnOao#M&W&888%bJnrx)?<!<_(~!I(*8&W&M###kdddZOphhpwp
1}l,+(Xc?1XLn{Ch*M#WWWW88%8hQvjj_:^^"!I;!l{*M*M##**kk*#bZh**k0m
]i;<-?)))YZx+?Qha#a#8888&%%#kwU]:Il:"^`^^":_a*#oh#appa#a#ba#bZk
<I+)]+--[x(_-1Qah###&888888Mhq/l>}t]~>I^``^;|baabdUXunnvLzLpbdk
_~-11ttjct}1(|OaaWM#W8%8&88Maw>+{_?<!l;^'`^,>JqbJ|}>^..I++(Zdmw
1{1numkzrf/n|_Xa*M*W#&&888&Mop_|+:^`'''...'""!Cw|v;' ',<zm
(fuCZhar)/j(~1QaMM#&#8&88W&#k(]_:I!;,,"^'.'`^":xul^. .f
{}rLmwQj(}!:^~Zo&M#W#W&&8&#hw)[<!~l`'. .``^^;-:`'
??{/rj(}!^. `1Zo&M#M#&&8&W*mJv/i_:,lI:^'..'`'``^,"`.
__++]]1, ^<Jph&##MM&8&W##Jnx/-!I+[[?+>:``' .''``'.
_+-?}+` '>{Qh**W##MW&WM*oYf(t]-]jcvuj{_!,`'...''`. .
?----, '`I?/QhM**#WWW&W##aU1{t??(cXzvr/)?i:"`..''''. . ...
_+~+>. `,~[upk##*W&&W&W#opx[]))|juczzvr/)?>:"^''.'''. . ....'
_++{I '`i[rckh*a*&&8&8Mkah|]-})(/fxczXznf1]>I:"`...''.........
]]]]. '^:_(vmhhooWMW88WMhhJ}~+-}1(/fnvzXXcr1?~!I,`...`''''''..'
(){}..^,+[jzpao*MM&&WWM#kwx?>>~]1)|/rucYYXcr1?_>l,'.'^^^^^`''''
(((,.'":_(nwdhao#MW8W#MkpU|_!!<-[)1)|rvYJUXvj{]_~l"'.`^,:;,```'
[{(`'"ll]j0qbka##WW&WMadUt?illi+]][[)/xzYUYznj)]_i;^..':!<l^``'
1))`'""!{cwkddaM#WWW*hqLr?!IIl!<_??]})tnvzXzvxu]]~l:^^"Ii}<"``'
)1{``,,}tUqddko*WMWMkmUf?l:::;I!~+_-]})/frnuuxx+]_>!IIl>!1!^'''
|1[^'::<|Cqmqbo*WM#hwjn]i,"^",I!<~+_-]}{1|/jrrr_??<!lI!ill,'..'
(1>`'^:>jObdmbk#*Mapn+_!,``^^";!<+++-?]]][{)|/}[]?>l;;Ill;^. .
{1l`'`I<uLdqdbh*MopC}i""'''`^":I>+_-?-_+++-][[~_?]iI:;;IlI^.
)(l```<[nJmppdk#akQr<^``..''`^,;!+-?]_~>ii<~~!Ii-]>I::;II;".
))l``^,?rOJqdmkhdwY|> '....'`^:l~-]_<!lll!!,:;l~[>l::;IIIl. .
({I``,,-ru0qpZ*dwQx~' . ...'^";>?]<l;:::;;^^":!->l;:;I;Il". .
)]I^`,,-juQ0LLapQu1I ..'`^:!-?l:,"""""`^^":_>!;:;I;;l!'..
)1I^^^"i)CJLLObJcr~' . ..`^,I__I"^^^^..'.'`"~>!I::;;;ll^..
))l"^^,>]vQJzwXOu}l. .. .''^";+~,^````'''. '`!>!I;;I;;II;..
1[I""^,I]jXxtOLvt_, .'...''`";<>,^``''^'. .',>!I;IIIIlll`
{_;""^^:-txxXCUf-I: .`'..''^":i!"^```.. .^i!l;IIIIll!"
[~::""^,_ttrJOx|+^^ ``''`^":i!^^^` '.. '>!lI;IIIll!i'
?<:,,"^,<})/Xx/[;'` .^''``^":i!""^'` .I!II;IlIIl!!'
}+:,,"^,i{frz)f[^'` .,``^^",;>i,"``' . .,!l;IIllIlll'
}+:,""",i[tLucn?,'' .,",:I<<;"'. .' ."!l;;Illl!!;.
1-:,""":<|uj(C)+^. 'I',:Il+_I^.`'. ."ilIIIll!!!:.
}+:,""",>]vxuu]+`.. '`"..:I!])i'`..` .:<!lIIll!!I`.
}~:,,,",l~/rLr?+"'.... . ''. "!~(u^^`''. ...'I+i!IIl!!l,..
]~::::,,I>}txY?>,^`'''.....^' 'i-j+"^^:" ...''^I+>!lll!!;..'
)_:::,:,;>?(rzji;""^^^`'````^:lIi{}`..'" ..''``^"l+>i!llll" '`
|?;:;:,:;i-{tux~l;:,,""^^^`"I"^,`_>`,`:`..''`^"",,i-~i!!ll:..`^
/[;:;:::;!_[(rX1illI;::,""""";"::'';'^,.'`^^",:,:;<?~>i!!l^ .^"
)?;;;:::;l<?1fcriii!lII;:::;I;l". .'.,``^"",,;;;;I<?+>i!!: `,^
[+;;;:,:Il<_[|ux<<>ii!lllIII:,!!Il`'>l:;;IIIIIlII:!<~>i!!,..,:"
}~;;::::Il>+])xzc-~>iiiiiii!!ii<!:;I<!!>>>iiiiii<,:><>i!!`.`;,"
)+;;;;::;li~?{r0qJ/+<<<><<~~+<!I:l;,ll>_++~~~++-}".I>!lli^'II::

Độ phân giải đã được giảm và bạn không thể thấy nhiều chi tiết như lúc trước nữa, nhưng điều này là cần thiết cho việc giảm số lượng ký tự ASCII.

Xử lý Aspect Ratio của ảnh

Những đôi mắt nhạy bén sẽ nhận ra rằng tỉ lệ ảnh đã không còn chính xác nữa. Mình đã xử lý các ký tự ASCII như thể nó là hình vuông giống pixel, nhưng thực sự các ký tự lại hình chữ nhật. Do đó, mình cần tính toán để resize lại bức ảnh để cho ra một ASCII art với tỉ lệ giống bức ảnh ban đầu.

Vì mình đã chọn một monospaced font, chiều rộng của mỗi ký tự sẽ bằng nhau. Bây giờ điều chúng ta cần làm là tính được aspect ratio của font. Vậy mình làm điều đó như thế nào? Mình không tìm ra giải pháp nào trong CSS thuần, vì thế mình đã thực hiện bằng một số thao tác DOM đơn giản dưới đây.

js
const getFontRatio = () => {
const pre = document.createElement('pre')
pre.style.display = 'inline'
pre.textContent = ' '
document.body.appendChild(pre)
const { width, height } = pre.getBoundingClientRect()
document.body.removeChild(pre)
return height / width
}
const fontRatio = getFontRatio()

Trick này sẽ add một thẻ <pre> (để giữ chính xác style) và tính toán kích thước bằng cách sử dụng function getBoundingClientRect().

Lưu ý là aspect ratio của font sẽ khác nhau khi bạn thay đổi font-size

Cho dù width của các ký tự bằng nhau đi chăng nữa thì một ký tự có width là 1px sẽ không làm cho 1000 ký tự xếp thành 1 hàng có width là 1000px. Đó là do tiến trình Kerning. Vậy nên cách của chúng ta nó chỉ giúp giảm thiểu sự chênh lệnh về aspect ratio chứ không chính xác 100%.

Cùng cập nhật clampDimensions function dựa trên font ratio.

diff
const clampDimensions = (width, height) => {
+ const rectifiedWidth = Math.floor(fontRatio * width);
+
if (height > MAXIMUM_HEIGHT) {
- const reducedWidth = Math.floor(width * MAXIMUM_HEIGHT / height);
+ const reducedWidth = Math.floor(rectifiedWidth * MAXIMUM_HEIGHT / height);
return [reducedWidth, MAXIMUM_HEIGHT];
}
if (width > MAXIMUM_WIDTH) {
- const reducedHeight = Math.floor(height * MAXIMUM_WIDTH / width);
+ const reducedHeight = Math.floor(height * MAXIMUM_WIDTH / rectifiedWidth);
return [MAXIMUM_WIDTH, reducedHeight];
}
- return [width, height];
+ return [rectifiedWidth, height];
};

Mình vừa tính toán size sản phẩm dựa trên font ratio và độ phân giải tối đa.

Và bây giờ, ASCII art của mình trông như một kiệt tác.

text
CXccxxcZbwJuvzCqkoohqLUJ0QJLpdZUvnXzXYLx(|ffz<}[<-__[~-!>l<>]-?])t)xcXXQOmOOqwqmpbppdddkhkqUxzOOYccJLCCQOmwpbkhakbbqm0CUCL0ZZOmwpdk*MMM&88
mQXx1?+1zqQr?~_tZbhoahhakOcvOpq0C0YLJnYvCXO1[(n)(}_1?~++~<]}{[1{)rvvJLCOQOqdphkkkpkbkbbbkkbqznJCCCZqwmLXXQmZZmwwwwmZOwpddpwmZ0Q0Owbaao*MWM
QOmwOX/)rZhL)<i~)zwbbbhoamYXULCQLmwOOOJmCpuuLcnuv{n/][++>]()||)/(/vJUXOCpZwdhakokhdkhhkhdkbkhmzUL0QOOQUcXOqqqqqwmwqdboabdpwOLCJQmpbbho**#*
CLLCQOLvxUpmf-!l!?fXOphohmYnuvUZwdpmZZbwbXUpqQJcfcn]11()11|(tfrjruvYCCJwZpqpdka**ookkhkkdaaabkwCUzcuUUYrjxULOqdbbka#M#kqmm0LJUJ0qdbkpmOZwm
XXQqbhbwmpamu1?_?}[?][|XZqwmqmmd#bb00ok#OQdbb0cuLC[jtr|{(rrxrfrnzzYcULOQqZqkkbho#M##abohkkkhdddOJLLufnYUUL0OZqbhkka#M*hbbhkdqmZmwqdmLULQLO
jfvLkMM*o*#dz/1{]-<><?)fz0qpdqmbbhpwkk&wwkohmZwmZrYnux/{1jzXunzzJUXzc0ZQmwqbhbho*WWWW#hkaha*a*dq0XUJzczczQwdbbppmmph#odZQOZmmZZZmwpka*#oo#
YJQ0ZZpkaoabZJJ00LzxrczUYJCQZda*bkbha&ppd##bwpqqLYcvXu(?/uYCcnvCQOQCYJ0pmqmqhbka#MM&&8&*akhh*aapdwQJQOmmwpbbdqOZOmpkhkqOQQZmmZZqdbkoMW&&88
QCL0O0OqdddqLx|tzZwOUuj//jrnqk#hakkoMhkh&WbdbdbdJQcuJu[+xzLQJrfCQ0Z0QQOqdwwpphaah*MM&&&W*hkka*ohapYfCwppkkho**apwqba**abqqpdpwqqqpbo##W&8%
ZZwOzrxUOmpqmCcXQqdbdpwZZqZQb#*aaaMaoka&WabkkbkqUZvzCn[<jzLwwnrYmCZZ0LZwbdwZbhhahhaMW&&8WMhahaobwhdpdhaaao**MWaqmph*#adqqpdpm00Omwdao*M8%%
YujftftfuUJ0wdwZmmZwqpwLznum*&M*p**MoaM&##akbdpQUmXxCX{~|C0wqv}f0qCmwmOpqbppdbhaooaoW&88%W*ahoo##ohahao######*dZOqbhhpZQLQQQQQQLCCOqkhoMW8
f)}}tzcxcQQQZOLCZpddbbdQr1|pM&*kkM*#oo&8M**ahkbLCqJuQLf_[UwdkQ(fvqqJqdqwkhdwkdkaooo**&&8%88#oh*ao**ohdbdbkkbbqOLOwppqOJzvucXUUC0mmZwpkao*W
ut|jzQwpqOCULQOwh#odOL0ZmqpoWa#**oM*hM8&*hoaohbOmdQYw0f?]vwbhd)1jrkpOpkdqkkdphhk**M#*#&8B%8&#Mh##M*#ddZO0OZZZLJCOmwqwZLUXXULOZZZZ0CJLmdka#
jt/())(/jfruU0ZphohdmZwdkkhWMWoaM*Wo#8&&M#oaoap0qdmCwOx[}(Lb*aU}|unbpmhhabhhhkaoa*WWM#M8%B%88##o**WodO00JYXYYXXCLXununncYC0ZwmOLUUQwdbbbho
Xzx(1[]})xCwdddppbddbhokpq*M&#**o&&*#8%&M####*mQpdZ0dZj1-}jwk*wt1)Ynqqp**adda*h*oba8W&##%%%%88M#o#aMadwQUcvvvvvuxnzCCJCQOZZZZO0QOmdkbdddba
qYf{[1fUmpqZJCOwpdbkhaakbd#W8M*##&*M&&&8&&MMMoZmdpZQqmr}-?}rdoav]]{/tCdmk#hkdak*a*aoMMMM#8%BB8&###M*opqLUXzvnrt|tuJQLUYUJCCLQ0OOZqkaakbdbk
0CznxYqhkqLntnCZpddwZmpdkh#W%W*#&%WM8%%88WWWWdOqbw0JwQf1)??/XpMqf[][1)(vwb**hbb****MMMMMM&8%BB8&WM***aadwOCUUJLCLLCUXvxuXCQO0QCL0mpbkkkkkh
ahhkh*Mo0n1})juYQmwOLLZmwpo&ho#*&M#&8BB%8&MW*mLmq0cumJt}][}{nOhh0/}[][{1tuLphhpwZOOQmbbdhko&%B%&&MM&#*hdOJutjYOmZLUUUYYYUUUUYXYCQ0Zwphoakk
MW&&&&#qc)][fzCOwqpddqOOwdaW%WM#&WM%B%B8&&MWkZOwpJXJbmLcunxrjvmaoCt?][{(ruvczXJ0LZCZOQQCOppaW%B%&&MWWohbwdkdmOLLQUzXYYXXXYJLQ0QLLLQQmbakdb
*MW&WMk0zcJOOYf)1/xYQYLwdda&&&MM&&M8%%B8&MoakdwwQLYULQUYJUXzunvC0v)]]})/frncJw*aokqvZkbOCUCma&BB%8&WWW#oaModm0Q0CzucXYYYYULZwmOQQQOqkaahkh
ko#ohkhbqZOZQv/1[?[jJQQQQZ##8W*#&WM%B%%%&om0XnjfffxvCqpbhdLUvnrf/1[?][1/jxuJYfLppw/|vXJzxfjcZo%@%88W&WMMh*ohqJntttxXUUXzccXUCL0OZmmqdkkkhk
kohq0Q0ZOCUvzn)+II<tQLvnYOdk88#*W*W%B%88WbQzuxrrnCZqCd*hapcYZvnrt([-+_[(fffxrr/)()1))(1[[[{/Xb%BB%8&W&#aakkkbdQvj||jcXXcvuuvcXXzcY0qk**abp
haaaahbpZz/{{]>:,i_)xXcrncUdh8&W8#&%B@88WdJxt/trvO0UjrLQCjfj/ft|(1]~ii~?}{1)(|()1{}[?+~<~~_}xw%B@%%&&&#abhkkbbbpLvrxuccvvvvczXXYJQwda*#*dZ
bkkhhkdZXf[~>>>><~~+]}[_++[tko&MWh&BB@B8&kUj(}]]}(tjff|111)))1}[]?_>I,;!<~__---_+~<i!ll!i<-1xm%B@@%8&&&ohdmOOmwJnttrucvuvuuucczzXUQwkahhhh
bbkbdpmc)?_-]}[[)/|}?++_??}tJh*WWoW%BB@%&*Qr1]_++__-?]]]]?--____++<!:^^"I!i>>>i!!llIIII!i~[|cdB@@@%8&W&#hbwLUC0Qzt)(rvcvunxxuuvczXJ0pbpqpk
bpZcrxLCc(]_?}}}1{}?_~<+--?}/Jp*W*#%%B@@%&kz)?+<<>>i>>><>>>>>><~<<>l:"^^:I!><<>i!lIIIII!>_}tU8@@@@B%%WWMahpmUnftttttfnvvuunnnnuczXUC0wqw0L
qOJf{}tzzj[~_{/({??]-+><~-[[rvmhMa#%BBBBB%8qx{_~<>i!!!l!!lll!i<~~<>l,^``";!i~-_+<i!!!!i>+?)nw%@@@@%%8&&Waddbq0UurxcuffxczcvuvczYCLCCCOmwqq
CXcvr|{{(t([--]1(|1[--]?[{f/))rO*#oM%%BBBB%%wx{?+~<>!!lllll!i<+?-~<I"`'''":I!?++++~<<<~+?{tUMBB@BB%888&&*hbbdqwwZOJXzXUYzcczYYJQZmmZZmZ0Op
ZO0OZQt{?[[}[?]|xcXcr([{[1((/xYZoM#W&8%%%%%%8pu([-+~>>i!!i>>~+_{?<<l,"^^,;i><>l!i>~~++-]}|vd%BB%B@%88%8&#ahpm0CUzczzcczccnucJLOmmO000Omqba
ULO00OCx)[??]?[/Y0OOOOQLJCL0QCLmoW**M&&%%88%8&dc/}]-++~<><~~~~~~][}(1]__-}j/]iIIl!i>~_?}(xm8%%B@B@%8&%8&WokqmZO0LL0LCUzuxfjxvYLmqqqwZ00wbd
(/f)[[}[?_+_?][)tt/jvznnYZwOQLOdhWM*#W#888&8&%8bzt)[?-++~~~<>i!i~}|fjt||(1]+!::I!<+_??}(rm8%BB@@B%8&8888WM#ooookpwmOZmmmOUnxuXC0mqqqwwmmZO
i>iI::::I>_}{1(/[+~?)/)fXwbbphhak*WM#MM&&888&&88*Uj(}]-_+~<>!!!ll!<-]{1{[??]}(/jvvxt||fvo8%BBB@BB%%&&888&*Mokbbkdm0OZZmqbkpmOOOZmmO0ZwwmZm
I!;::li<?1|/t/)}?}(rzJCQmddqdhkkZoWM*#M&8W88&8&&88hct1[?_++++____-(|nXLLXcvvXXvt}_>~tn0aM8BB@BBB%88&&88&&MW*kdbhhdmOOOZmqbbbbpmOZwqpw0LQOZ
[]]}1)11[_~+?}}??)zZpdbpqw0UXcvOLa#M##M&&&WW&888&&88aUj({[[}1|jXJYvnrxuuc/~;,^^```^";!tp*W8B@@%%88W&M888WMW&#khaahbdppppbbbdppqwwqkahdqpdb
+?1rv|]-<I:;l!li+{nU0mwZQUzvx(nLCk##WWM&WW&8M&88%&&88%8oUxfttfftffffnJx[<>>!!!iiii!I;:;l_k#8B@B8&&&W#&&&&WMM&#hhahkbdddppqppdbdpdbkhhhkbbb
_}jzr+l;!<_{|(1)/([?[|xttnXJzxzCwa**W#M#M&&&&&W&888888%%BWk0Xuxxxxxr{_+-]]~--+)rxt]+<i!I;l[q88%&&M8oW&&&W#**#&MhkkdppdbdqZ00Zwpkaahbwmmwqp
({{?+l"";<-{/vJJc{_+[/cJ0OCx)}frQb*oo#a*##W&W&&W&&&&88B8%%&*hdQzurjruvc<:,^^^^``^^",:I~~il;I!qaM&#*##WMMMa&M#ok*kpkha##M*bmOwka****ohqQC0w
{-<l;:l<_---?{(|){{fY0mZYt]~<-fXZb*#M#**o*#&8&&M&W&8&&88%88W*hbdwOUu[l,:;IllI;,,"^^`^^^^^^",::i)0aooM###*a*oWMhhbpmhaao*#ooohdbka*#obmOmbh
~iI;!_}()[+~~~__+~-1jj|]_++_?}xvZdo*M*#&**oM&8888W888888888&#ahbpm[l;I!~-[|)[?+~<<>!;,"^```^^^":!uqboaoohkkbdqZz|)f[xurnrv0QzuvJOwppbbpdka
+~><+_]{({[1t|)|jcYXr){[{((|()vJmd*o#WM**#M*#&888888&8&8888&Moabq(>i<_?)/~I,""^^^^",:,^````^^`^,;;irddkaqOYx1_][I,".. ..,!>_xCOqkkqZZmm
1)){}1|xxjuLbokOzjrxrt(juv/]1tjcpb*#WMM***WWMW&&&&88888W88&&M*okp(_+[rv<;,"^^```'''''........'^^^"":ixLpZX-]j[,`. '-rnJZq
1jj/jvJCQOmba*kCf{1///ft1?--~[jLbbo#WMWo#WWM*M&&&&8888&&W&WM#okdx{?{|?;:;li>>``'''''^",^''....'``^"",,lrLxr(!,^`'. 'Ir
1}][)rXQmqpqqmQzf||/(->lI^..^<zzpha*WWo*M#M&o#M&8&&8&&&&WM*o#dmY|?]1<:Ii+?l'`''... .'``````^^",,{}!:"`''.
[]??[{(/frrft|1}}[~;' '":[z0qMo*WM&##M#&#&8W8&&8&W&&#MhqkCJUXcu>l>+[,`^":Il;:,"^^`'.. ...'''``````^^^^","^`'..
_____+~+_?]]}[]>, . .^;~)YwwdMoMM#M#WWM8M8&W88W&&&MW*#b0kuvnnxj++?)I:l>+?]{|1]?-+~>!:"^```'.....'''''`'`^^''.. .
--_+_-?]}}?]?l. .;!+[]jQqdha*W*W*M###8&8MW8&W&&W#Ma#d0qf/t|/j}?[<_-]{ncccvvuuxf/1[?~>I:"^`''.. .'''''`^`.. .
???--??]?_]l '`^:i?_{rnmqbMo###*#M#MWWW8&W8&W&W#*o#*wOr(1{11j[?}[-[(nzXXXXzcunrf/|){]+<!;:,"^``'.....''''``' . . ......'
-_++~~~_?_" .^"^;-?{}1U0wqb*aM#W**&#&&&&88&8MWWMa#abbmX/1]}[[{1)//tfrxuvczzXXXzvnrjt|({]-~!;,""^^``'.'.'''''''.... . . .........'
__+++~_[}; ..^^`"i+?}/jXXOdbaa*M#o*&WMW&W8W&888MW*akaabQj{[-???[}1(|(|/tfjrnuczzXXzzvuxf/({[-+<!I;:,,"``'.......'`....................'
?]]]?[}}!. .``;",!+]{fjvYZhkkaa#oaa&M*WWW&88WWMMM#aaakaqc|]+<~~_-?[}())(||/tjxuvcczXXXXzzvnj/{[-+~>iII;:,^'.......'``''''.....''...'...'
())1){1]^. ``^,I:i-}(xrxUZqbhhhaMoo&M*8WW&8&&MMWMMokhpp0Jf{-<<i>><_?[}1))(||/fjrnuvzXYYYYXXzvnr/{[??-+~>il:"^`....'`^^^^````^^```''''''''
(((|(//<'.''`":::I?{(vxYmmphbkkh#oaWM*WWWW8&&&WM#**hdqmJUf1?_i!!!i>~_?]}11{{11)|/jxnvzYUJJJJYXcuxj/{[[]?+~>iI;,^`'.'```^",,::;;:,^``'''''`
[[}{1(]^'.`",,::l~(tjvXqwwkpkkaooa#W#MW&M&&&W&MM#adpOLzn([_<!!IIl!i>~_-]}][[][}1)|tfxvzYYJUUYYXzvnxj/{[]?-+<i!;,^`'....'^:Il><>iI,^```````
)1))|(]`''^^^";;>?}{z0qqpbdqhhhkoo###M#W&W&MW**appOLXx(-+>lI;IIIIll!><+_??]??][}{1(/trnuvczXXXzccvnrrj)}}[]_<ilI:,^^^^`":Il![{}?l,"^`'```'
{)111)i`''"`"::I~]+/CmOpbkqqqdkoa*o*#*WWWW#MoodZQXur)-<lI;::::::;IIl!i>~++___-??[}{)(//tfjjxnnuuvuunrx)+~[[[?~>>i!llIII!><il<)xiI,^`'''''`
((()}}^`''`^"":~if)vLmwkbOmpbkahoa*M*MWMW*akbQrnjf{?+i;,"""^""",,:;Ili>~~~++++__-?][}}{11)(|//fjjjrrjrn}~i-}?+<i!!!lIIl!i>ilI~i;"`''.....'
()1{[[``'`'`""i<+i{XYmkkhqdZkkkooaMMMM#*akdpU(~<-_i!;"^`'````^^"",:I!!>~~+++~~__-??]]]]]]][[}}1)((||/|]]}}[-?~>!III;;;IIllllI;I"`.... .
}}{)(_^`'^```":![}cOLwbbqqppbdkhkM****akpwJf1_<:`;I,^'''''''`^^^",:;Ili>~_--__-???--_++++++__-?]][{{-+<+?[[?_~>l;;;::::;IIIII;;;'... .
)))||:^````^^:;;+{fUCddq0Oddpkpk**#*obdZQcj1~;,. I^`...'..'`'''`^^,,;II!<+-???]]?-+~~<>iiiii>>~++_-Ill!i>~]--+>!l;;:::::;;IllIII:... .
)))()"^``''^";;<>[UUcZJwwdhqqmqa**akdqOOYr/<i". .`..........'''``^",:Ili~_???[?+<>i!!llIlllll!!,":;::;Il!~??+>!lI;:::;;;;;IIIIIl,.. ...
((1{}"^`````:,,!;}UvYwOZqdOdQmpahhbwOOJcu(>>' ...........''`^^",:;!>+-[{?+>!II;;::::::;;;,^^""^,",:;!_-~>ilI;;:,:;;;I;;;;ll!". .....
(1]-~"^^^```":,I>(znuZLQmOQZQJdhbwZQJXUx1_!' . .. ......''``^",:Ii<_}r_iI;:,""",""""""^`:,^'^^^^",;>-+<i!lI;:::;;I;;;;Ill!l^......
)))1>""^^```^,,l??t/rzCQmLCUCwdqZmCJzuf}_I^ ... ....'''``^",:li+}u<l:,""^^^^^``^^ ''.'.''``^"l~~>i!!I;;;:;;;;;;;;IIlll'.....
)(|)!,""""^`^^,;<~}1fXOQUzXXzqJOwvCjj/?[~: ... ....''`^^"":;!<[xi;,"`^^````''^^'.'' '.....''`,!<>i!lII;;;;IIII;;;IIll!!'....
)1{{I,,""""^^^""!>](rJYzvrXjCZCJcuuu}]";:` .'''.......'''``^^",:I>]|l:"^```'`''' .,:'' .. .'`:>ii!!ll;;;;IIIIIIIIllll!l.
{}[]I,,",,""^`^,>!+||xc/{ft1QCUJzXz[-!',l ':".......'''``^"",:I!_[;,^^``````^`. .'^liii!lIIII;IIIlIllllllll!;.
[]??I:,,,,,,"^^^:Ii1/f({)/||0Ufjfv}]>,`i, .`''.''''```^^"",:I!_-;,^^`^```. .'`. .. .'"i!i!lII;;IIIIIIIIIIll!!!iI'
???-I:,,,,,""^^^"Ii_]{[u|trvLx)rcf+I:` "^ ..'^`^`'````^^"",:;!__;,"^^^^` '",`. ' '`ii!lIII;;IIlllIIlIIIll!!i!:
{{{[l,:,,,""""^":;i]-[/|zvnXnjxXc(li:'.:. .^``'.^^`^^^""",:;Ii-?I:,"""''^"^. ... .':!!!lII;;IIIllllIIll!ll!!I:.
}[}]l::,,,,""""",:l_?(frUvntrtxz(_l"`' .. `"""",,::;Il>-|i;:,"'.. ' ..' ."ii!lIIII;IIllllllllll!ll:'
111}l:,,,,"""""^";!+?{xjXuntrYvt{+i^`. .. . . .,i;`',,:;;;Ili~]J~!;^ ..''' ''. .'^ii!!lIII;;IIllllll!!!!l;^
}{{[!:,,,""""""^:Ii<-/uxjnrjxx|1?>!;''. . .'. `"^`. `:;IIli<-(O]>" ^,,`. .`'. ..'"I>i!!llIIIIIlllll!!!!I:^ .
}}[?l:::,,,",""",:;!-{[jxrXCvc}?_<iI^`''.... . ''`. ';l!>~-{xQj^',` . '^^` ....'^:!_>i!!llIIIIll!!!!llI:. .'
[[]_l;::::,,,,,,,:;l<+[{|jxXcJn?~>!;""^```''''''''.....'..'^^` .. .',i<+?{fY_^^^.`:'."`" .........'''`^",>}~>ii!llllIl!l!!!l;, ..''
1))[l;:::::,,,,,::;Ii<-[1|trnzL(+>!I:,,"""^""^^^`'``````^^`':":`";lll:"+]1jz<:. ^ '",` ....''''`````^^",,I~{+<>ii!llll!l!llII^. .'``
)||[!;;;;:::::::::;I!>+?[1(truzC[~>!I;:::,,,,,,"""^^^^^``^```^;;:'^::"'._j/l.'^,:`.^::^.....'''``^^^""""",,,,;!_1_~<>ii!!!!!!!ll;' .`^"^
|/f1!;::;;;::,,,;:;Il>~_?}{(trnzO]_>!llllI;;;;;:,,,,""""""""^`^::l^`;lI;,`..,l". 'l,;'..''```^^^""",,,:::,::;li?1_+~<>i!!!!!!lIl ..`""""
)(({!;;;;;;:::::;;;Il!<~_?[1(trvUC|-!iii!!!!llIIIII;:::::::;;II;liil""^.....'.`':I!,''``^^"",,,,,:::;;;;;;;;I!>_]-+~<>iiii!!l!, .`","^^
[[]-l;:::;;;::,::;;Il!>~+-][1|fnXCx{><<>iiii!!!!l!lllllIII;;;;IIIli<il;I!I,^..`I!I",:;;III;;I;IIIIIIIlIIIIIIII:;<_+~<<iii!!!!^. ..`,;:""^
{[[-lI::;;:::::::;;Il!i<~+-]})/rcCc0n[_~<>>>iiiiiiiiiiiiiii!!!!!iii><<>I,^:lll!>i<<<<>>>>>>>iiiiiiiiiiii!i>!"^:I+~<>>iii!!ii;' .^:I;,"""
()}-l:;:;;:;:,::;;;Ill!><~_-]{(jvCwqwZQY1]+<<<>>>>><<<<<<<<~~~+~+~!;I:,,;!:,""":Ili<+_+++++++~~~~~~~++__-?]<^...';i>i!llll!iI,`.`,I!l:,,,:

Tốt hơn nhiều rồi đúng không? Giảm font-size cũng giúp bạn nhìn bức tranh dễ dàng hơn.

Để tăng độ phân giải thì chúng ta sẽ tăng MAXIMUM_HEIGHTMAXIMUM_WIDTH. Để tăng dải màu thì chúng ta tăng độ dài của chuỗi grayRamp. Không biết các bạn thấy thế nào chứ với mình nhìn thấy thì với độ phân giải thấp thì ta nên chọn dải màu ít để hình dễ nhìn hơn (mặc dầu có thể mất đi chi tiết).

Dưới đây là kết quả của dải màu với 10 cấp độ gray. Anh em so sánh nhé.

text
++====+##*+=++*####*+++*++*#*+==+++++=-=-=::::::::.:...:::::---=+++*********##*######*==+*+==++++*****######***++++*******##%%%%%%
*++=-::-**=:.:-*########+=+***+*+++=+++=+::+---:-::::.::-:---===+++***################*=++++*****++*************##*********####%%%
*****=--+#*=:..-+*######+++++++******+**=++-+=-=-::::::-------=+++*+***#################+++****++++**********#####***+++**########
++++**+==**=:...:=+*####+===+**#****##++*#**====:---------=====++++*****#################*++==+++===++**#####%%#****++++*####*****
++**###**##+-:::-:::-=+******####**###*+##*+=++:=-=---======+++=++****######%%%########*#*++*=-=++++***######%#######*****#**++*+*
===*#%%####+---::..::-=+**#**###**###**###****=+===---=++==+++++=******#####%%%%#########**++++=+=+**###****####*+*********###%##%
++****#####*++**+===++++++*########%**#%#*****++=+=-:-+++==++**+++******####%%%%%%#########**++*****###*****###**********###%%%%%%
*++****###**=--+***+=---==*#######%###%%*####+++=+=::=+*+=-+***********######%%%%%%#########+=+**########***#####***#*****###%%%%%
****===+****+++**###*******#%###%####%%######+*=++=::==**+==*+***+**##**######%%%%%%######*##*########%%#**#####***#*******###%%%%
+==----=++**#*********+==+#%%####%##%%#%####*+*+=+=::=+**+--+*+****#*#**#######%%%%%%#####%#######%%%%%#***####**++*++**+++*###%%%
=-:-=+==+****++**####*+--+#%###%####%%######*+*+=*+-:-**#*--=**+*#**###*########%%%%%%#####%###########********++===++++*****####%
=--=+****+++***#%#**+****#%#%###%##%%%######**#*+*+-:-+*##=:==##**##*########%###%%%%%%###%#%#*#*******++******++++++*****+++**##%
=------====+***###****####%%##%#%#%%%%%#####**#*+**=::-###*:-==#**###########%%%%%%%%%%%%###%##***+++++++++======++*****+++**#####
++=--::--+**##**#######*#%%%###%%#%%%%#%#%%***#**#*--:-=###=--+=**############%%%%%%%%%%%%###%##*++=========++++***********#######
*+-::-=****++***#########%%%%#%%#%%%%%%%%%%**#*****-:::-+##*::---+#*###########%%%%%%%%%%%%#%###*++++===--=++++++++++*****########
*+===+###*=-=+**##***####%%%##%%%%%%%%%%%%#**#**+*+--:::=*##=:::---+*#%#######%%%%%%%%%%%%%%######**++++++++++===++***++***#######
######%#+-:--=+****+****#%##%#%%%%%%%%%%%%#+***+=*+-:::--+##*=-:::--==*###******######%%%%%%%%%###*+=-=+***++++++++++++++****#####
%%%%%%#+=::-++***##*****##%%%#%%%%%%%%%%%#****+++#*+======+##*=:::--====++***+***++**##%%%%%%%%###*##***+*+++++++++++**+++++*#####
#%%%%#*+=+**+=---=+*+**##%%%%%%%%%%%%%%####***++++++++++===+*+-:::--===+*####*=*##*+++*#%%%%%%%%###%#*****+==+++++++*****+**######
########****+--:::=+**+*#%%%#%%%%%%%%%%#*+===-===+*###*++===--::::--===+=+*#*=-=++====+*%%%%%%%%%%####*+=--==++++==+++*******#####
###*****++++=-:..:=*+==+*#%%%#%#%%%%%%%*+=====+**+####*=++===-:::::-=-====--------:::-=+#%%%%%%%%######*+=---=+++====++++++*#####*
#######*+=--:.. .:-=+===+*#%%%%#%%%%%%#*+=--==**+==**+-==-=---::..:::--------:::::.:::-+#%%%%%%%%########*+==========+++++**##%##*
######*+=-:.....:::::::::-*#%%%#%%%%%%%*=--:::--==---------::::.. ...::::::::.......::-+#%%%%%%%%###*****+=-==========+++++*######
#####**=:::::::---:::::::-=##%%#%%%%%%%#+-::::::::::::::::::::.. ................:--+#%%%%%%%%###*+++*+=--===========++++*##**#
#**===*+=:::::---::::.::::-=##%#%%%%%%%%*=:::..............:..... ................:-=*%%%%%%%%%%##**+=------===========+++*****+
**+---=+=-:::---:::::..:::-=*#%#%%%%%%%%#+-::..............:::.. ...:::........::-+#%%%%%%%%%%####*++=====-==+====+++++++*****
++===------:::----:::::------+#%#%%%%%%%%#+-:::...........::::.. ...:::::...:::-=*%%%%%%%%%%%#####****+++++++==++++**********
*****+-:::::::-==+==-:::---=+*#%%%%%%%%%%%#+-:::.........:::::.. .........:::::--+%%%%%%%%%%%%###**++++++==++===++**********##
++****+--:::::-+*****++++**++*#%##%%%%%%%%%#+--::::....::::.:::--::::-=-:......::::-=#%%%%%%%%%%%%%##****++**+++======+*********##
----::::::::::----=++==+***+**#%%#%%%%%%%%%%#+=-::::::::.....:-==-----:.. ...:::::-=#%%%%%%%%%%%%%%%%#####********+===++**********
..... ..::----::::---=+##*#####%%%%%%%%%%%%%%+=-:::::.........::---::::---====---=#%%%%%%%%%%%%%%%#%#####*******##***************
... ...::------::-=+++**#*####*#%%#%%%%%%%%%%%%#=--:::::::::::--=++++==+++=-:..-=*#%%%%%%%%%%%%%%%%%%######*******####********++**
::::----:::::-::-+*###***+++=+*##%#%%%%%%%%%%%%%%#+=-:::---=++=======-. ..=##%%%%%%%%%%%%%%%%%%#######****####*****####**##
::-==-::.......:-=+****++==--++###%%%%%%%%%%%%%%%%%%#+==----=====+=:...............-#%%%%%%%%%#%%%%%%%%#########*****###*#########
:-=+-....::------:::-=-==++==+*###%#%#%%%%%%%%%%%%%%%%%*+=======-:::::.:::-==::......-%%%%%%%#%%%%%###%%####**###******#####******
---::. .::-=++=-:::=++**+-:-=+####%##%%%%%%%%%%%%%%%%%%##*+=======. .:.....:##%%###%%%%#%%####*####%%##***#######**+**
-:.....::::::-----=+***=-:.:-+*###%%###%%%%%%%%%%%%%%%%%%###**+=:. ..... .:=###%%%#####%####*#####%########%##***##
.....:---:::::::::-==-::::::-+*###%#%%###%%%%%%%%%%%%%%%%####*-....::---:::..... .:*#########**+=---======*+==++***##*###
:..::::--:-----==++=-::-----=+*###%%%##%%#%%%%%%%%%%%%%%%###*=...::--. . ...-*###**+=:::.. ..-+**##*****
---:--====+###*+====--===-:-==*###%%%##%%%%%%%%%%%%%%%%%%####=:::==.. .=+**=:-=: .==+**
-==-=+++**####+=-------:::::-+####%%%#%%%#%%%%%%%%%%%%%%%###+-:--: ..... .=+==:. .=
-:::-=+*******+=----:... =+*###%%##%#%%#%%%%%%%%%%%%###**-::-....:: -:.
:::::---===----::. .=+*%##%%%#%%%%%%%%%%%%%%%%%#**+++==...:: ....
::::::::::::::. .:=***%#%%%%%%%%%%%%%%%%%%%%##**+=====:::-....:::--:::::...
::::::::::::. .:::-+*####%#%#%#%%%%%%%%%%%%#%##**+-=--=-::.:::-+========--::...
::::::::::. .:::-=**#%#%%%#%%#%%%%%%%%%%%%##%#*+-----=:::::--=++++++====----::...
:::::.:::. .::-:=+**###%#%#%%%%%%%%%%%%%%#%##**=-::-:------======+++++====----::..
::::::::: .::--=++#####%###%%%%%%%%%%%%%######+-:::::::-------=====++++++===----:::....
:::::::- . .::-=-++#####%###%%%%%%%%%%%%%%#####*=-:.:.:::::-------======+++++++===-::::.....
-------. . .:--===**#####%##%%#%%%%%%%%%%%###***=-:::....:::--------=====+++++++++===-::::::...
------- .:--==+**########%%#%%%%%%%%%%####**+=-::......::::---------====+++++++++===--:::::.... ...
::----. .:-===+***#######%%#%%%%%%%%%%%##**+==-::........::::::::::----===+++++++++====--:::::... ........
------. ..::-+***##*########%%%%%%%%%###**++=-::............:::::::::-----=====+++++======---:::.... ...-:::.
-----: . .:::=***###***########%%%%####**+==-:.... .. .......::::::::::------=============-::::::..............:--..
----:: ..--++**##**#######%#%%%%###*====-::.. ......:::::::::::::---------========::.::::..............:.
----:. ..:.-++*###**######%%%%#####*-:.::... .....:::::::::::::::::::::--------::--:::.................
-----. .:-+*+*##***#*###%######**=-:.. .. .....::::::::::::::::::::::::--::.::::::.................
----- ..:-=++##*+*#*#########**+=-:.. . .....:::::::::::.........::::......:::::..... . .........
----: ...:++=****##***######***==-.. ....::::::::.............. .. ....::::..... ...........
----: ..:+=+***#*****####***+==:.. ....:::::....... . .... ..::....... ...........
--::. ..-===*+*****+*##**++++--.. ...::--.... . ..::....... ............
----. .::--=++**+++*#***++==--.. ...:-:.. .:.....................
----. ..::-=+**=++=**+*=+==-::. ..:-:. .......................
---- ..:-=+++===+*+++===:: . ..:-:. ........................
-::: ..:--==--=-+++++++::. . ...:. ........................
:::: ..--------+*-===::. . ...:. .........................
:::: ..::--=-==+=--==:. ...:. .........................
---:. ..::--=+===-==+-... ....:.. ........................
::::.. .::-==*===-==-:. ....:-:.. ........................
---:.. ..::-==+===++=-:. .. .......:=-.. .......................
---:. ..::========--:... .....::==:. .....................
-:::.. ...:--===+=+-::... ....::-++. .:...................
::::.. . ..:::--==+++::... ..::-==. .-:................
---:.. ...:::--===+=:... . ..... ::-==. .-::...............
---:....... .....::---===+-:...... . . . :=- . ..-::.............
---:....... ......:::---===*::............ .. .... .. . . ...:-:::...........
---:....... .......:::---==++-:.................. ........ .. ............::::..........
::::........ .......::::---=++=:.................................... ... ......................... .:::......... ..
-:::........ . ........::::--==+=*=:::.............................. ................................ ..:.......... ..
---:........ ........::::---=+****+=-::...............::::::.... ... ....::::::::::::::::::::::. .......... .... .

Tóm lại

Dưới đây là các link liên quan:

Vậy là trong bài viết này chúng ta đã tạo được một tool khá thú vị, thông qua đó chúng ta cũng học được khá nhiều liên quan đến việc xử lý ảnh. Chúc các bạn làm thành công nhé.

Tham khảo