##
# SimpleCaptcha
# Simple image-based macthematical captcha
#
# @version 2.6.0
# https://github.com/foo123/simple-captcha
#
##
import math, random, base64, hmac, hashlib, zlib, struct
class SimpleCaptcha:
"""
SimpleCaptcha
https://github.com/foo123/simple-captcha
"""
VERSION = '2.6.0'
def __init__(self):
self.captcha = None
self.hmac = None
self.opts = {}
self.option('secret_key', 'SECRET_KEY')
self.option('secret_salt', 'SECRET_SALT_')
self.option('difficulty', 1) # 0 (very easy) to 3 (more difficult)
self.option('distortion_type', 1) # distortion type: 1: position distortion, 2: scale distortion
self.option('distortion', None) # distortion amplitudes by difficulty
self.option('num_terms', 2) # default
self.option('max_num_terms', -1) # default, same as num_terms
self.option('min_term', 1) # default
self.option('max_term', 20) # default
self.option('has_multiplication', True) # default
self.option('has_division', True) # default
self.option('has_equal_sign', True) # default
self.option('color', 0x121212) # text color
self.option('background', 0xffffff) # background color
def option(self, *args):
nargs = len(args)
if 1 == nargs:
key = str(args[0])
return self.opts[key] if key in self.opts else None
elif 1 < nargs:
key = str(args[0])
val = args[1]
self.opts[key] = val
return self
def getCaptcha(self):
if not self.captcha: self.generate()
return self.captcha
def getHash(self):
if not self.captcha: self.generate()
return self.hmac
def reset(self):
self.captcha = None
self.hmac = None
return self
def validate(self, answer = None, hmac = None):
if (answer is None) or (hmac is None): return False
hash = createHash(str(self.option('secret_key')), str(self.option('secret_salt') if self.option('secret_salt') else '') + str(answer))
return hash_equals(hash, hmac)
def generate(self):
difficulty = min(3, max(0, int(self.option('difficulty'))))
distortion_type = min(2, max(0, self.option('distortion_type')))
distortion = self.option('distortion')
num_terms = max(1, int(self.option('num_terms')))
max_num_terms = int(self.option('max_num_terms'))
min_term = max(0, int(self.option('min_term')))
max_term = max(0, int(self.option('max_term')))
has_mult = bool(self.option('has_multiplication'))
has_div = bool(self.option('has_division'))
has_equal = bool(self.option('has_equal_sign'))
color = self.option('color')
background = self.option('background')
if (not isinstance(color, list)) and (not callable(color)): color = [color]
if (not isinstance(background, list)) and (not callable(background)): background = [background]
if isinstance(color, list): color = list(map(lambda x: int(x), color))
if isinstance(background, list): background = list(map(lambda x: int(x), background))
if max_num_terms > num_terms:
num_terms = rand(num_terms, max_num_terms)
# generate mathematical formula
formula, result = self.formula(num_terms, min_term, max_term, has_mult, has_div, has_equal, difficulty)
# compute hmac of result
self.hmac = createHash(str(self.option('secret_key')), str(self.option('secret_salt') if self.option('secret_salt') else '') + str(result))
# create image captcha with formula depending on difficulty
captcha, width, height = self.image(formula, color, background, difficulty, distortion_type, distortion)
# output image
self.captcha = imagepng(captcha, width, height)
return self
def formula(self, terms, min, max, has_mult, has_div, has_equal, difficulty):
# generate mathematical formula
formula = []
result = 0
factor = 0
divider = 0
for i in range(terms):
x = rand(min, max)
if (result > x) and rand(0, 1):
# randomly use plus or minus operator
x = -x
elif has_mult and (x <= 10) and rand(0, 1):
# randomly use multiplication factor
factor = rand(2, 3)
elif has_div and (0 == x % 2) and rand(0, 1):
# randomly use division factor
divider = rand(2, 3) if 0 == x % 3 else 2
if 0 < factor:
result += x * factor
if 0 > x:
formula.append('-')
formula.extend(split(abs(x)))
formula.append('×')
formula.extend(split(factor))
else:
if 0 < i: formula.append('+')
formula.extend(split(x))
formula.append('×')
formula.extend(split(factor))
elif 0 < divider:
result += math.floor(x / divider)
if 0 > x:
formula.append('-')
formula.extend(split(abs(x)))
formula.append('÷')
formula.extend(split(divider))
else:
if 0 < i: formula.append('+')
formula.extend(split(x))
formula.append('÷')
formula.extend(split(divider))
else:
result += x
if 0 > x:
formula.append('-')
formula.extend(split(abs(x)))
else:
if 0 < i: formula.append('+')
formula.extend(split(x))
factor = 0
divider = 0
if has_equal:
formula.append('=')
formula.append('?')
return (formula, result)
def image(self, chars, color, background, difficulty, distortion_type, distortion):
bitmaps = _chars()
cw = bitmaps['width']
ch = bitmaps['height']
n = len(chars)
space = 1
x0 = 10
y0 = 10
w = n * cw + (n-1) * space + 2 * x0
h = ch + 2 * y0
wh = w*h
# img bitmap
imgb = [0] * wh
img = [0] * (wh << 2)
x1 = 0
y1 = h/2
x2 = w-1
y2 = h/2
x = 0
y = 0;
j = 0;
for i in range(wh):
if x >= w:
x = 0
y += 1
c = colorAt(x, y, background, x1, y1, x2, y2)
j = i << 2;
img[j + 0] = c[0]
img[j + 1] = c[1]
img[j + 2] = c[2]
img[j + 3] = 255
x += 1
# render chars
for c in chars:
charbmp = bitmaps['chars'][c]['bitmap']
x1 = 0
y1 = rand(0, ch-1)
x2 = cw-1
y2 = rand(0, ch-1)
for x in range(cw):
for y in range(ch):
alpha = charbmp[x + cw*y]
if 0 < alpha:
imgb[x0+x + w*(y0+y)] = alpha
x0 += cw + space
if (0 < difficulty) and (0 < distortion_type):
if 2 == distortion_type:
# create scale-distorted image data based on difficulty level
phase = float(rand(0, 2)) * 3.14 / 2.0
amplitude = float(distortion[str(difficulty)]) if isinstance(distortion, dict) and (str(difficulty) in distortion) else (0.5 if 3 == difficulty else (0.25 if 2 == difficulty else 0.15))
x0 = max(0, round((w - n*(1.0+amplitude)*cw - (n-1)*space) / 2))
for k in range(n):
scale = (1.0 + amplitude * math.sin(phase + 6.28 * 2 * k / n))
sw = min(w, round(scale * cw))
sh = min(h, round(scale * ch))
y0 = max(0, round((h - sh) / 2))
x1 = 0
y1 = sh/2
x2 = sw
y2 = sh/2
for ys in range(sh):
y = max(0, min(h-1, round(10 + ys / scale)))
for xs in range(sw):
x = max(0, min(w-1, round(10 + k*(cw+space) + xs / scale)))
alpha = imgb[x + y*w]
if 0 < alpha:
alpha /= 255.0
c = colorAt(xs, ys, color, x1, y1, x2, y2)
j = ((x0+xs + (y0+ys)*w) << 2)
img[j ] = clamp(img[j ]*(1-alpha) + alpha*c[0])
img[j+1] = clamp(img[j+1]*(1-alpha) + alpha*c[1])
img[j+2] = clamp(img[j+2]*(1-alpha) + alpha*c[2])
x0 += space + sw
else:
# create position-distorted image data based on difficulty level
phase = float(rand(0, 2)) * 3.14 / 2.0
amplitude = float(distortion[str(difficulty)]) if isinstance(distortion, dict) and (str(difficulty) in distortion) else (5.0 if 3 == difficulty else (3.0 if 2 == difficulty else 1.5))
yw = 0
x1 = 0
y1 = ch/2
x2 = cw
y2 = ch/2
for y in range(h):
y0 = y
for x in range(w):
x0 = x
y0 = max(0, min(h-1, round(y + amplitude * math.sin(phase + 6.28 * 2.0 * x / w))))
alpha = imgb[x0 + y0*w]
if 0 < alpha:
alpha /= 255.0
xc = x - 10 + space - math.floor((x - 10 + space)/(cw + space))*(cw + space)
yc = y - 10
c = colorAt(xc, yc, color, x1, y1, x2, y2)
j = ((x + yw) << 2)
img[j ] = clamp(img[j ]*(1-alpha) + alpha*c[0])
img[j+1] = clamp(img[j+1]*(1-alpha) + alpha*c[1])
img[j+2] = clamp(img[j+2]*(1-alpha) + alpha*c[2])
yw += w
else:
# create non-distorted image data
x1 = 0
y1 = ch/2
x2 = cw
y2 = ch/2
yw = 0
for y in range(h):
for x in range(w):
i = x + yw
alpha = imgb[i]
if 0 < alpha:
alpha /= 255.0
# x = x0 + i*cw + (i-1)*space + xc
# xc = x - x0 + space - i*(cw + space)
xc = x - 10 + space - math.floor((x - 10 + space)/(cw + space))*(cw + space)
yc = y - 10
c = colorAt(xc, yc, color, x1, y1, x2, y2)
j = (i << 2)
img[j ] = clamp(img[j ]*(1-alpha) + alpha*c[0])
img[j+1] = clamp(img[j+1]*(1-alpha) + alpha*c[1])
img[j+2] = clamp(img[j+2]*(1-alpha) + alpha*c[2])
yw += w
# free memory
bitmaps = None
imgb = None
return (img, w, h)
def rand(m, M):
return random.randrange(m, M+1)
def split(s):
return [c for c in str(s)]
def hash_equals(h1, h2):
n1 = len(h1)
n2 = len(h2)
n = max(n1, n2)
res = True
for i in range(n):
if i >= n1:
res = res and False
elif i >= n2:
res = res and False
else:
res = res and (h1[i] == h2[i])
return res
def createHash(key, data):
return str(hmac.new(bytes(str(key), 'utf-8'), msg=bytes(str(data), 'utf-8'), digestmod=hashlib.sha256).hexdigest())
def imagepng(img, width, height, metaData=dict()):
return 'data:image/png;base64,' + base64.b64encode(PNGPacker(metaData).toPNG(img, width, height)).decode("ascii")
def colorAt(x, y, colors, x1, y1, x2, y2):
#if isinstance(colors, dict) and ('image' in colors) and ('width' in colors) and ('height' in colors): return patternAt(x, y, colors)
if callable(colors): return colors(x, y)
# linear gradient interpolation between colors
dx = x2 - x1
dy = y2 - y1
vert = 0 == dx
hor = 0 == dy
f = 2*dx*dy
l = len(colors) - 1
px = x - x1
py = y - y1
t = 0 if hor and vert else (py/dy if vert else (px/dx if hor else (px*dy + py*dx)/f))
if 0 >= t:
c0 = c1 = 0
t = 0
elif 1 <= t:
c0 = c1 = l
t = 1
else:
c0 = math.floor(l*t)
c1 = c0 if l == c0 else (c0 + 1)
rgb0 = colors[c0]
rgb1 = colors[c1]
t = (l*t - c0)/(c1 - c0) if c1 > c0 else t
return [
clamp((1-t)*((rgb0 >> 16) & 255) + t*((rgb1 >> 16) & 255)),
clamp((1-t)*((rgb0 >> 8) & 255) + t*((rgb1 >> 8) & 255)),
clamp((1-t)*((rgb0) & 255) + t*((rgb1) & 255))
]
#def patternAt(x, y, pattern):
# x = round(x) % pattern['width']
# y = round(y) % pattern['height']
# if 0 > x: x += pattern['width']
# if 0 > y: y += pattern['height']
# i = (x + y*pattern['width']) << 2
# return [
# pattern['image'][i + 0],
# pattern['image'][i + 1],
# pattern['image'][i + 2]
# ]
def _chars():
return {
"fontSize": 20,
"width": 12,
"height": 15,
"chars": {
"0": {
"width": 12,
"height": 15,
"bitmap": [
0,
0,
0,
222,
255,
255,
255,
222,
0,
0,
0,
0,
0,
0,
222,
255,
255,
255,
255,
255,
255,
0,
0,
0,
0,
94,
255,
255,
0,
0,
0,
222,
255,
139,
0,
0,
0,
182,
255,
48,
0,
0,
0,
48,
255,
182,
0,
0,
0,
222,
255,
0,
0,
0,
0,
0,
222,
255,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
255,
222,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
255,
222,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
255,
222,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
255,
222,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
222,
255,
0,
0,
0,
0,
0,
222,
255,
0,
0,
0,
182,
255,
48,
0,
0,
0,
0,
255,
222,
0,
0,
0,
94,
255,
255,
0,
0,
0,
222,
255,
139,
0,
0,
0,
0,
255,
255,
255,
255,
255,
255,
255,
0,
0,
0,
0,
0,
0,
222,
255,
255,
255,
255,
0,
0,
0,
0
]
},
"1": {
"width": 12,
"height": 15,
"bitmap": [
0,
0,
0,
0,
0,
0,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
222,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
222,
255,
255,
139,
0,
0,
0,
0,
0,
0,
94,
255,
255,
222,
255,
139,
0,
0,
0,
0,
0,
0,
222,
255,
48,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0
]
},
"2": {
"width": 12,
"height": 15,
"bitmap": [
0,
0,
0,
255,
255,
255,
255,
182,
0,
0,
0,
0,
0,
48,
255,
255,
255,
255,
255,
255,
255,
0,
0,
0,
0,
222,
255,
139,
0,
0,
0,
222,
255,
139,
0,
0,
0,
255,
182,
0,
0,
0,
0,
48,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
0,
48,
255,
182,
0,
0,
0,
0,
0,
0,
0,
0,
0,
139,
255,
94,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
182,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
222,
255,
182,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
182,
0,
0,
0,
0,
0,
0,
0,
0,
48,
255,
255,
255,
255,
255,
255,
255,
255,
255,
0,
0,
94,
255,
255,
255,
255,
255,
255,
255,
255,
255,
0,
0
]
},
"3": {
"width": 12,
"height": 15,
"bitmap": [
0,
0,
0,
222,
255,
255,
255,
139,
0,
0,
0,
0,
0,
0,
255,
255,
255,
255,
255,
255,
182,
0,
0,
0,
0,
139,
255,
182,
0,
0,
0,
255,
255,
48,
0,
0,
0,
222,
255,
0,
0,
0,
0,
139,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
139,
255,
94,
0,
0,
0,
0,
0,
0,
0,
0,
48,
255,
255,
0,
0,
0,
0,
0,
0,
0,
139,
255,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
139,
255,
255,
255,
255,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
222,
255,
48,
0,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
255,
222,
0,
0,
0,
0,
0,
222,
255,
0,
0,
0,
182,
255,
139,
0,
0,
0,
139,
255,
182,
0,
0,
0,
0,
255,
255,
255,
255,
255,
255,
255,
0,
0,
0,
0,
0,
0,
222,
255,
255,
255,
182,
0,
0,
0,
0
]
},
"4": {
"width": 12,
"height": 15,
"bitmap": [
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
255,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
139,
255,
255,
139,
0,
0,
0,
0,
0,
0,
0,
48,
255,
182,
255,
139,
0,
0,
0,
0,
0,
0,
0,
222,
182,
94,
255,
139,
0,
0,
0,
0,
0,
0,
139,
255,
48,
94,
255,
139,
0,
0,
0,
0,
0,
48,
255,
139,
0,
94,
255,
139,
0,
0,
0,
0,
0,
222,
222,
0,
0,
94,
255,
139,
0,
0,
0,
0,
139,
255,
48,
0,
0,
94,
255,
139,
0,
0,
0,
48,
255,
139,
0,
0,
0,
94,
255,
139,
0,
0,
0,
139,
255,
255,
255,
255,
255,
255,
255,
255,
255,
48,
0,
139,
255,
255,
255,
255,
255,
255,
255,
255,
255,
48,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0
]
},
"5": {
"width": 12,
"height": 15,
"bitmap": [
0,
0,
94,
255,
255,
255,
255,
255,
255,
139,
0,
0,
0,
0,
182,
255,
255,
255,
255,
255,
255,
139,
0,
0,
0,
0,
222,
222,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
255,
182,
0,
0,
0,
0,
0,
0,
0,
0,
0,
48,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
94,
255,
255,
255,
222,
0,
0,
0,
0,
0,
139,
255,
255,
255,
255,
255,
255,
255,
0,
0,
0,
0,
182,
255,
139,
0,
0,
0,
94,
255,
182,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
222,
255,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
255,
222,
0,
0,
0,
0,
0,
222,
255,
0,
0,
0,
182,
255,
139,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
255,
255,
255,
255,
255,
255,
222,
0,
0,
0,
0,
0,
0,
222,
255,
255,
255,
182,
0,
0,
0,
0
]
},
"6": {
"width": 12,
"height": 15,
"bitmap": [
0,
0,
0,
94,
255,
255,
255,
222,
0,
0,
0,
0,
0,
0,
182,
255,
255,
255,
255,
255,
255,
0,
0,
0,
0,
94,
255,
182,
0,
0,
0,
182,
255,
139,
0,
0,
0,
222,
255,
48,
0,
0,
0,
0,
255,
222,
0,
0,
0,
255,
182,
0,
0,
0,
0,
0,
0,
0,
0,
0,
48,
255,
139,
0,
255,
255,
255,
222,
0,
0,
0,
0,
94,
255,
222,
255,
255,
255,
255,
255,
255,
0,
0,
0,
94,
255,
255,
222,
0,
0,
0,
139,
255,
182,
0,
0,
94,
255,
255,
0,
0,
0,
0,
0,
222,
255,
0,
0,
94,
255,
182,
0,
0,
0,
0,
0,
182,
255,
48,
0,
48,
255,
222,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
255,
255,
48,
0,
0,
0,
0,
222,
255,
0,
0,
0,
139,
255,
222,
0,
0,
0,
139,
255,
182,
0,
0,
0,
0,
222,
255,
255,
255,
255,
255,
255,
0,
0,
0,
0,
0,
0,
139,
255,
255,
255,
182,
0,
0,
0,
0
]
},
"7": {
"width": 12,
"height": 15,
"bitmap": [
0,
255,
255,
255,
255,
255,
255,
255,
255,
255,
48,
0,
0,
255,
255,
255,
255,
255,
255,
255,
255,
255,
48,
0,
0,
0,
0,
0,
0,
0,
0,
139,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
48,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
182,
0,
0,
0,
0,
0,
0,
0,
0,
0,
222,
255,
48,
0,
0,
0,
0,
0,
0,
0,
0,
48,
255,
182,
0,
0,
0,
0,
0,
0,
0,
0,
0,
139,
255,
94,
0,
0,
0,
0,
0,
0,
0,
0,
0,
222,
255,
0,
0,
0,
0,
0,
0,
0,
0,
0,
48,
255,
182,
0,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
0,
0,
0,
0,
0,
0,
0,
222,
255,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
222,
255,
0,
0,
0,
0,
0,
0,
0
]
},
"8": {
"width": 12,
"height": 15,
"bitmap": [
0,
0,
0,
182,
255,
255,
255,
182,
0,
0,
0,
0,
0,
0,
222,
255,
255,
255,
255,
255,
222,
0,
0,
0,
0,
94,
255,
182,
0,
0,
0,
182,
255,
139,
0,
0,
0,
139,
255,
94,
0,
0,
0,
48,
255,
182,
0,
0,
0,
139,
255,
94,
0,
0,
0,
48,
255,
182,
0,
0,
0,
48,
255,
182,
0,
0,
0,
139,
255,
94,
0,
0,
0,
0,
94,
255,
255,
255,
255,
255,
94,
0,
0,
0,
0,
0,
182,
255,
255,
255,
255,
255,
182,
0,
0,
0,
0,
139,
255,
139,
0,
0,
0,
94,
255,
182,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
222,
255,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
255,
222,
0,
0,
0,
0,
0,
222,
255,
48,
0,
0,
182,
255,
139,
0,
0,
0,
94,
255,
222,
0,
0,
0,
0,
255,
255,
255,
255,
255,
255,
255,
48,
0,
0,
0,
0,
0,
182,
255,
255,
255,
222,
0,
0,
0,
0
]
},
"9": {
"width": 12,
"height": 15,
"bitmap": [
0,
0,
0,
182,
255,
255,
255,
139,
0,
0,
0,
0,
0,
0,
222,
255,
255,
255,
255,
255,
182,
0,
0,
0,
0,
139,
255,
182,
0,
0,
0,
139,
255,
94,
0,
0,
0,
222,
255,
0,
0,
0,
0,
0,
222,
222,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
182,
255,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
255,
255,
0,
0,
0,
0,
0,
222,
255,
48,
0,
0,
182,
255,
182,
0,
0,
0,
182,
255,
255,
48,
0,
0,
0,
255,
255,
255,
255,
255,
255,
222,
255,
48,
0,
0,
0,
0,
222,
255,
255,
255,
0,
182,
255,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
222,
222,
0,
0,
0,
182,
255,
48,
0,
0,
0,
48,
255,
182,
0,
0,
0,
139,
255,
222,
0,
0,
0,
222,
255,
48,
0,
0,
0,
0,
255,
255,
255,
255,
255,
255,
139,
0,
0,
0,
0,
0,
0,
222,
255,
255,
255,
94,
0,
0,
0,
0
]
},
"+": {
"width": 12,
"height": 10,
"bitmap": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
0,
222,
255,
255,
255,
255,
255,
255,
255,
255,
182,
0,
0,
222,
255,
255,
255,
255,
255,
255,
255,
255,
182,
0,
0,
0,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
"-": {
"width": 7,
"height": 2,
"bitmap": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
222,
255,
255,
255,
255,
182,
0,
0,
0,
0,
0,
0,
222,
255,
255,
255,
255,
182,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
"×": {
"width": 12,
"height": 9,
"bitmap": [
0,
0,
222,
48,
0,
0,
0,
0,
182,
94,
0,
0,
0,
0,
255,
255,
48,
0,
0,
139,
255,
139,
0,
0,
0,
0,
0,
255,
255,
0,
139,
255,
182,
0,
0,
0,
0,
0,
0,
48,
255,
255,
255,
182,
0,
0,
0,
0,
0,
0,
0,
0,
94,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
48,
255,
255,
255,
182,
0,
0,
0,
0,
0,
0,
0,
255,
255,
0,
139,
255,
182,
0,
0,
0,
0,
0,
255,
255,
48,
0,
0,
139,
255,
139,
0,
0,
0,
0,
222,
48,
0,
0,
0,
0,
182,
94,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
"÷": {
"width": 11,
"height": 8,
"bitmap": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
255,
255,
255,
255,
255,
255,
255,
255,
0,
0,
182,
255,
255,
255,
255,
255,
255,
255,
255,
255,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
"=": {
"width": 12,
"height": 6,
"bitmap": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
222,
255,
255,
255,
255,
255,
255,
255,
255,
139,
0,
0,
222,
255,
255,
255,
255,
255,
255,
255,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
222,
255,
255,
255,
255,
255,
255,
255,
255,
139,
0,
0,
222,
255,
255,
255,
255,
255,
255,
255,
255,
139,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
"?": {
"width": 12,
"height": 15,
"bitmap": [
0,
0,
0,
222,
255,
255,
255,
255,
0,
0,
0,
0,
0,
0,
255,
255,
255,
255,
255,
255,
255,
94,
0,
0,
0,
182,
255,
182,
0,
0,
0,
139,
255,
255,
0,
0,
0,
255,
222,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
48,
0,
0,
0,
0,
0,
0,
0,
0,
139,
255,
182,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
182,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
48,
255,
222,
0,
0,
0,
0,
0,
0,
0,
0,
0,
139,
255,
94,
0,
0,
0,
0,
0,
0,
0,
0,
0,
139,
255,
94,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
139,
255,
94,
0,
0,
0,
0,
0,
0,
0,
0,
0,
139,
255,
94,
0,
0,
0,
0,
0
]
}
}
}
# PNG utilities
PNG_SIGNATURE = b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
# color-type bits
COLORTYPE_GRAYSCALE = 0
COLORTYPE_PALETTE = 1
COLORTYPE_COLOR = 2
COLORTYPE_ALPHA = 4 # e.g. grayscale and alpha
# color-type combinations
COLORTYPE_PALETTE_COLOR = 3
COLORTYPE_COLOR_ALPHA = 6
COLORTYPE_TO_BPP_MAP = {
'0': 1,
'2': 3,
'3': 1,
'4': 2,
'6': 4
}
GAMMA_DIVISION = 100000
def clamp(value):
return max(0, min(255, round(value)))
def paethPredictor(left, above, upLeft):
paeth = left + above - upLeft
pLeft = abs(paeth - left)
pAbove = abs(paeth - above)
pUpLeft = abs(paeth - upLeft)
if pLeft <= pAbove and pLeft <= pUpLeft: return left
if pAbove <= pUpLeft: return above
return upLeft
def filterNone(pxData, pxPos, byteWidth, rawData, rawPos, bpp):
rawData[rawPos:rawPos+byteWidth] = pxData[pxPos:pxPos+byteWidth]
def filterSumNone(pxData, pxPos, byteWidth, bpp):
sum = 0
for i in range(pxPos, pxPos + byteWidth):
sum += abs(pxData[i])
return sum
def filterSub(pxData, pxPos, byteWidth, rawData, rawPos, bpp):
for x in range(byteWidth):
left = pxData[pxPos + x - bpp] if x >= bpp else 0
val = pxData[pxPos + x] - left
rawData[rawPos + x] = ubyte(val)
def filterSumSub(pxData, pxPos, byteWidth, bpp):
sum = 0
for x in range(byteWidth):
left = pxData[pxPos + x - bpp] if x >= bpp else 0
val = pxData[pxPos + x] - left
sum += abs(val)
return sum
def filterUp(pxData, pxPos, byteWidth, rawData, rawPos, bpp):
for x in range(byteWidth):
up = pxData[pxPos + x - byteWidth] if pxPos > 0 else 0
val = pxData[pxPos + x] - up
rawData[rawPos + x] = ubyte(val)
def filterSumUp(pxData, pxPos, byteWidth, bpp):
sum = 0
for x in range(pxPos, pxPos + byteWidth):
up = pxData[x - byteWidth] if pxPos > 0 else 0
val = pxData[x] - up
sum += abs(val)
return sum
def filterAvg(pxData, pxPos, byteWidth, rawData, rawPos, bpp):
for x in range(byteWidth):
left = pxData[pxPos + x - bpp] if x >= bpp else 0
up = pxData[pxPos + x - byteWidth] if pxPos > 0 else 0
val = pxData[pxPos + x] - ((left + up) >> 1)
rawData[rawPos + x] = ubyte(val)
def filterSumAvg(pxData, pxPos, byteWidth, bpp):
sum = 0
for x in range(byteWidth):
left = pxData[pxPos + x - bpp] if x >= bpp else 0
up = pxData[pxPos + x - byteWidth] if pxPos > 0 else 0
val = pxData[pxPos + x] - ((left + up) >> 1)
sum += abs(val)
return sum
def filterPaeth(pxData, pxPos, byteWidth, rawData, rawPos, bpp):
for x in range(byteWidth):
left = pxData[pxPos + x - bpp] if x >= bpp else 0
up = pxData[pxPos + x - byteWidth] if pxPos > 0 else 0
upleft = pxData[pxPos + x - (byteWidth + bpp)] if pxPos > 0 and x >= bpp else 0
val = pxData[pxPos + x] - paethPredictor(left, up, upleft)
rawData[rawPos + x] = ubyte(val)
def filterSumPaeth(pxData, pxPos, byteWidth, bpp):
sum = 0
for x in range(byteWidth):
left = pxData[pxPos + x - bpp] if x >= bpp else 0
up = pxData[pxPos + x - byteWidth] if pxPos > 0 else 0
upleft = pxData[pxPos + x - (byteWidth + bpp)] if pxPos > 0 and x >= bpp else 0
val = pxData[pxPos + x] - paethPredictor(left, up, upleft)
sum += abs(val)
return sum
def deflate(data, compressionLevel=-1, chunkSize=None):
#chunkSize = 16*1024 if chunkSize is None else chunkSize
compressor = zlib.compressobj(level=compressionLevel)
zdata = compressor.compress(data)
zdata += compressor.flush()
return zdata
def crc32(data):
return zlib.crc32(data)
def ubyte(value):
return value & 255
def I1(value):
return struct.pack('!B', value & 255)
def I4(value):
return struct.pack('!I', value & 0xffffffff)
def i4(value):
return struct.pack('!i', value)
class PNGPacker:
def __init__(self, options=dict()):
options['deflateChunkSize'] = max(1024, int(options['deflateChunkSize'] if ('deflateChunkSize' in options) else 32 * 1024))
options['deflateLevel'] = min(9, max(0, int(options['deflateLevel'] if ('deflateLevel' in options) else 9)))
options['deflateStrategy'] = min(3, max(0, int(options['deflateStrategy'] if ('deflateStrategy' in options) else 3)))
options['inputHasAlpha'] = bool(options['inputHasAlpha'] if ('inputHasAlpha' in options) else True)
options['bitDepth'] = 8 #int(options['bitDepth'] if 'bitDepth' in options else 8)
options['colorType'] = min(6, max(0, int(options['colorType'] if ('colorType' in options) else COLORTYPE_COLOR_ALPHA)))
if (options['colorType'] != COLORTYPE_COLOR) and (options['colorType'] != COLORTYPE_COLOR_ALPHA):
raise Exception('option color type:' + str(options['colorType']) + ' is not supported at present')
#if options['bitDepth'] != 8:
# raise Exception('option bit depth:' + str(options['bitDepth']) + ' is not supported at present')
self._options = options
def toPNG(self, data, width, height):
# Signature
png = PNG_SIGNATURE
# Header
png += self.packIHDR(width, height)
# gAMA
if 'gamma' in self._options:
png += self.packGAMA(self._options['gamma'])
# filter data
filteredData = self.filterData(data, width, height)
# compress data
deflateOpts = self.getDeflateOptions()
compressedData = deflate(bytes(filteredData), deflateOpts['level'], deflateOpts['chunkSize'])
filteredData = None
# Data
png += self.packIDAT(compressedData)
compressedData = None
# End
png += self.packIEND()
return png
def getDeflateOptions(self):
return {
'chunkSize': self._options['deflateChunkSize'],
'level': self._options['deflateLevel'],
'strategy': self._options['deflateStrategy']
}
def filterData(self, data, width, height):
# convert to correct format for filtering (e.g. right bpp and bit depth)
# and filter pixel data
return self._filter(self._bitPack(data, width, height), width, height)
def packIHDR(self, width, height):
IHDR = I4(width) + I4(height)
IHDR += I1(self._options['bitDepth']) # bit depth
IHDR += I1(self._options['colorType']) # color type
IHDR += I1(0) # compression
IHDR += I1(0) # filter
IHDR += I1(0) # interlace
return self._packChunk('IHDR', IHDR)
def packGAMA(self, gamma):
return self._packChunk('gAMA', I4(math.floor(float(gamma) * GAMMA_DIVISION)))
def packIDAT(self, data):
return self._packChunk('IDAT', data)
def packIEND(self):
return self._packChunk('IEND', None)
def _bitPack(self, data, width, height):
outHasAlpha = ('colorType' in self._options) and self._options['colorType'] == COLORTYPE_COLOR_ALPHA
inputHasAlpha = ('inputHasAlpha' in self._options) and bool(self._options['inputHasAlpha'])
if inputHasAlpha and outHasAlpha: return data
if (not inputHasAlpha) and (not outHasAlpha): return data
outBpp = 4 if outHasAlpha else 3
outData = [0] * (width * height * outBpp)
inBpp = 4 if inputHasAlpha else 3
inIndex = 0
outIndex = 0
bgColor = self._options['bgColor'] if 'bgColor' in self._options else {}
bgRed = clamp(bgColor['red'] if 'red' in bgColor else 255)
bgGreen = clamp(bgColor['green'] if 'green' in bgColor else 255)
bgBlue = clamp(bgColor['blue'] if 'blue' in bgColor else 255)
for y in range(height):
for x in range(width):
red = data[inIndex]
green = data[inIndex + 1]
blue = data[inIndex + 2]
if inputHasAlpha:
alpha = data[inIndex + 3]
if not outHasAlpha:
alpha = float(alpha) / 255.0
red = (1 - alpha) * bgRed + alpha * red
green = (1 - alpha) * bgGreen + alpha * green
blue = (1 - alpha) * bgBlue + alpha * blue
else:
alpha = 255
outData[outIndex] = clamp(red)
outData[outIndex + 1] = clamp(green)
outData[outIndex + 2] = clamp(blue)
if outHasAlpha: outData[outIndex + 3] = clamp(alpha)
inIndex += inBpp
outIndex += outBpp
return outData
def _filter(self, pxData, width, height):
filters = [
filterNone,
filterSub,
filterUp,
filterAvg,
filterPaeth
]
filterSums = [
filterSumNone,
filterSumSub,
filterSumUp,
filterSumAvg,
filterSumPaeth
]
filterTypes = [0] # make it default
#if (not 'filterType' in self._options) or (self._options['filterType'] == -1):
# filterTypes = [0, 1, 2, 3, 4]
#elif int(self._options['filterType']) == self._options['filterType']:
# filterTypes = [self._options['filterType']]
#else:
# raise Exception('unrecognised filter types')
bpp = COLORTYPE_TO_BPP_MAP[str(self._options['colorType'])]
byteWidth = width * bpp
rawPos = 0
pxPos = 0
rawData = [0] * ((byteWidth + 1) * height)
sel = filterTypes[0]
n = len(filterTypes)
for y in range(height):
if n > 1:
# find best filter for this line (with lowest sum of values)
min = math.inf
for i in range(n):
sum = filterSums[filterTypes[i]](pxData, pxPos, byteWidth, bpp)
if sum < min:
sel = filterTypes[i]
min = sum
rawData[rawPos] = sel
rawPos += 1
filters[sel](pxData, pxPos, byteWidth, rawData, rawPos, bpp)
rawPos += byteWidth
pxPos += byteWidth
return rawData
def _packChunk(self, type, data = None):
block = str(type).encode('ascii')
length = 0
if data is not None:
if isinstance(data, list): data = bytes(data)
length = len(data)
block += data
return I4(length) + block + I4(crc32(block))
__all__ = ['SimpleCaptcha']
|