←︎ scroll :: 2d8110b


1
commit 2d8110b256a344bbcf1c7a5b96e148b975378a98
2
Author: acidvegas <acid.vegas@acid.vegas>
3
Date:   Sun Feb 16 16:43:02 2020 -0500
4
5
    Fixed remote exploit reading file:// and fixed floatints
6
---
7
 README.md                      |   2 +-
8
 scroll/core/ascii2png.py       |   2 +-
9
 scroll/core/config.py          |  10 +-
10
 scroll/core/constants.py       |   2 +-
11
 scroll/core/database.py        |   4 +-
12
 scroll/core/debug.py           |   5 +-
13
 scroll/core/functions.py       |   2 +-
14
 scroll/core/irc.py             |  14 +--
15
 scroll/core/mircart/canvas.py  |  82 ++++++++++++++++
16
 scroll/core/mircart/mircart.py | 216 +++++++++++++++++++++++++++++++++++++++++
17
 scroll/core/mircart/png.py     |  60 ++++++++++++
18
 scroll/scroll.py               |   2 +-
19
 12 files changed, 379 insertions(+), 22 deletions(-)
20
21
diff --git a/README.md b/README.md
22
index ded3b6e..c011a50 100644
23
--- a/README.md
24
+++ b/README.md
25
@@ -5,7 +5,7 @@
26
 
27
 ## Requirements
28
 - [Python](https://www.python.org/downloads/) *(**Note:** This script was developed to be used with the latest version of Python)*
29
-- [Pillow](https://pypi.org/project/Pillow/) *(Required by [core/ascii2png.py](scroll/core/ascii2png.py))*
30
+- [Pillow](https://pypi.org/project/Pillow/) *(Required by [core/mircart.py](scroll/core/mircart.py))*
31
 
32
 ## Setup
33
 Edit the [core/config.py](scroll/core/config.py) file and then place your art files in the [data/art](scroll/data/art) directory.
34
diff --git a/scroll/core/ascii2png.py b/scroll/core/ascii2png.py
35
index 1194d61..7c82486 100644
36
--- a/scroll/core/ascii2png.py
37
+++ b/scroll/core/ascii2png.py
38
@@ -1,6 +1,6 @@
39
 #!/usr/bin/env python
40
 # -*- coding: utf-8 -*-
41
-# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
42
+# Scroll IRC Art Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
43
 # ascii2png.py
44
 
45
 '''
46
diff --git a/scroll/core/config.py b/scroll/core/config.py
47
index fecab55..61fbf6a 100644
48
--- a/scroll/core/config.py
49
+++ b/scroll/core/config.py
50
@@ -1,13 +1,13 @@
51
 #!/usr/bin/env python
52
-# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
53
+# Scroll IRC Art Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
54
 # config.py
55
 
56
 class connection:
57
 	server     = 'irc.server.com'
58
-	port       = 6697
59
-	proxy      = None
60
+	port       = 6667
61
+	proxy      = None # Must be in IP:PORT format (Socks5 proxies only)
62
 	ipv6       = False
63
-	ssl        = True
64
+	ssl        = False
65
 	ssl_verify = False
66
 	vhost      = None
67
 	channel    = '#chats'
68
@@ -29,7 +29,7 @@ class login:
69
 	operator = None
70
 
71
 class settings:
72
-	admin = 'nick!user@host' # Must be in nick!user@host format (Can use wildcards here)
73
+	admin = 'nick!user@host' # Must be in nick!user@host format (Wildcards accepted)
74
 	log   = False
75
 	modes = None
76
 
77
diff --git a/scroll/core/constants.py b/scroll/core/constants.py
78
index 03d3d52..109eb9a 100644
79
--- a/scroll/core/constants.py
80
+++ b/scroll/core/constants.py
81
@@ -1,5 +1,5 @@
82
 #!/usr/bin/env python
83
-# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
84
+# Scroll IRC Art Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
85
 # constants.py
86
 
87
 # Control Characters
88
diff --git a/scroll/core/database.py b/scroll/core/database.py
89
index d4b8f8d..d302349 100644
90
--- a/scroll/core/database.py
91
+++ b/scroll/core/database.py
92
@@ -1,5 +1,5 @@
93
 #!/usr/bin/env python
94
-# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
95
+# Scroll IRC Art Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
96
 # database.py
97
 
98
 import os
99
@@ -20,10 +20,10 @@ def check():
100
 		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('max_results',     '10'))
101
 		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('max_uploads',     '25'))
102
 		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('max_uploads_per', '5'))
103
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('rnd_exclude',     'ansi,big,birds,hang,pokemon'))
104
 		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('throttle_cmd',    '3'))
105
 		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('throttle_msg',    '0.03'))
106
 		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('throttle_png',    '0.03'))
107
-		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('rnd_exclude',     'ansi,big,birds,hang,pokemon'))
108
 		db.commit()
109
 
110
 class Ignore:
111
diff --git a/scroll/core/debug.py b/scroll/core/debug.py
112
index 5c29f15..2f43707 100644
113
--- a/scroll/core/debug.py
114
+++ b/scroll/core/debug.py
115
@@ -1,12 +1,11 @@
116
 #!/usr/bin/env python
117
-# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
118
+# Scroll IRC Art Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
119
 # debug.py
120
 
121
 import ctypes
122
 import logging
123
 import os
124
 import sys
125
-import time
126
 
127
 from logging.handlers import RotatingFileHandler
128
 
129
@@ -37,7 +36,7 @@ def info():
130
 	clear()
131
 	print('#'*56)
132
 	print('#{0}#'.format(''.center(54)))
133
-	print('#{0}#'.format('Scroll IRC Bot'.center(54)))
134
+	print('#{0}#'.format('Scroll IRC Art Bot'.center(54)))
135
 	print('#{0}#'.format('Developed by acidvegas in Python'.center(54)))
136
 	print('#{0}#'.format('https://acid.vegas/scroll'.center(54)))
137
 	print('#{0}#'.format(''.center(54)))
138
diff --git a/scroll/core/functions.py b/scroll/core/functions.py
139
index 148a5d7..6c7b13a 100644
140
--- a/scroll/core/functions.py
141
+++ b/scroll/core/functions.py
142
@@ -1,5 +1,5 @@
143
 #!/usr/bin/env python
144
-# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
145
+# Scroll IRC Art Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
146
 # functions.py
147
 
148
 import base64
149
diff --git a/scroll/core/irc.py b/scroll/core/irc.py
150
index 51f2fd7..afb0547 100644
151
--- a/scroll/core/irc.py
152
+++ b/scroll/core/irc.py
153
@@ -1,5 +1,5 @@
154
 #!/usr/bin/env python
155
-# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
156
+# Scroll IRC Art Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
157
 # irc.py
158
 
159
 import glob
160
@@ -108,7 +108,7 @@ class Commands:
161
 		try:
162
 			Bot.playing = True
163
 			data = open(ascii_file, encoding='utf8', errors='replace').read()
164
-			if len(data.splitlines()) > int(database.Settings.get('max_lines')) and chan != '#scroll':
165
+			if len(data.splitlines()) > functions.floatint(database.Settings.get('max_lines')) and chan != '#scroll':
166
 				Commands.error(chan, 'File is too big.', 'Take it to #scroll')
167
 			else:
168
 				name = ascii_file.split(ascii_dir)[1]
169
@@ -131,7 +131,7 @@ class Commands:
170
 
171
 	def sendmsg(target, msg):
172
 		Commands.raw(f'PRIVMSG {target} :{msg}')
173
-		time.sleep(functions.floatint(database.Settings.get('msg_throttle')))
174
+		time.sleep(functions.floatint(database.Settings.get('throttle_msg')))
175
 
176
 class Events:
177
 	def connect():
178
@@ -166,7 +166,7 @@ class Events:
179
 			elif args[0] == '.ascii' and not database.Ignore.check(ident):
180
 				if Bot.playing and msg == '.ascii stop':
181
 					Bot.stopper = True
182
-				elif time.time() - Bot.last < int(database.Settings.get('cmd_throttle')) and not functions.is_admin(ident):
183
+				elif time.time() - Bot.last < int(database.Settings.get('throttle_cmd')) and not functions.is_admin(ident):
184
 					if not Bot.slow:
185
 						Commands.error(chan, 'Slow down nerd!')
186
 						Bot.slow = True
187
@@ -192,7 +192,7 @@ class Events:
188
 							Commands.error(chan, 'Invalid URL.', 'Only PasteBin & TermBin URLs can be used.')
189
 					elif args[1] == 'remote' and len(args) == 3:
190
 						url = args[2]
191
-						if 'pastebin.com/raw/' in url or 'termbin.com/' in url:
192
+						if url.startswith('https://pastebin.com/raw/') or url.startswith('https://termbin.com/'):
193
 							data = functions.get_source(url)
194
 							if data:
195
 								for item in data.split('\n'):
196
@@ -270,7 +270,7 @@ class Events:
197
 								Commands.error(chan, 'Invalid file name.', 'Use ".ascii list" for a list of valid file names.')
198
 					Bot.last = time.time()
199
 		except Exception as ex:
200
-			if time.time() - Bot.last < int(database.Settings.get('cmd_throttle')):
201
+			if time.time() - Bot.last < int(database.Settings.get('throttle_cmd')):
202
 				if not Bot.slow:
203
 					Commands.sendmsg(chan, color('Slow down nerd!', constants.red))
204
 					Bot.slow = True
205
@@ -282,7 +282,7 @@ class Events:
206
 		if functions.is_admin(ident):
207
 			args = msg.split()
208
 			if msg == '.update':
209
-				output = functions.cmd(f'git -C {ascii_dir} reset --hard FETCH_HEAD')
210
+				output = functions.cmd(f'git -C {ascii_dir} pull')
211
 				if output:
212
 					for line in output.split('\n'):
213
 						Commands.sendmsg(chan, line)
214
diff --git a/scroll/core/mircart/canvas.py b/scroll/core/mircart/canvas.py
215
new file mode 100644
216
index 0000000..1da08d5
217
--- /dev/null
218
+++ b/scroll/core/mircart/canvas.py
219
@@ -0,0 +1,82 @@
220
+#!/usr/bin/env python
221
+
222
+class store():
223
+	def flip_cell_state(self, cellState, bit):
224
+		if cellState & bit:
225
+			return cellState & ~bit
226
+		else:
227
+			return cellState | bit
228
+
229
+	def parse_char(self, colourSpec, curColours):
230
+		if len(colourSpec) > 0:
231
+			colourSpec = colourSpec.split(',')
232
+			if len(colourSpec) == 2 and len(colourSpec[1]) > 0:
233
+				return (int(colourSpec[0] or curColours[0]), int(colourSpec[1]))
234
+			elif len(colourSpec) == 1 or len(colourSpec[1]) == 0:
235
+				return (int(colourSpec[0]), curColours[1])
236
+		else:
237
+			return (15, 1)
238
+
239
+	def importTextFile(self, pathName):
240
+		self.inFile = open(pathName, 'r', encoding='utf-8')
241
+		self.inSize = self.outMap = None;
242
+		inCurColourSpec = ''; inCurRow = -1;
243
+		inLine = self.inFile.readline()
244
+		inSize = [0, 0]; outMap = []; inMaxCols = 0;
245
+		while inLine:
246
+			inCellState = 0x00
247
+			inParseState = 1
248
+			inCurCol = 0; inMaxCol = len(inLine);
249
+			inCurColourDigits = 0; inCurColours = (15, 1); inCurColourSpec = '';
250
+			inCurRow += 1; outMap.append([]); inRowCols = 0; inSize[1] += 1;
251
+			while inCurCol < inMaxCol:
252
+				inChar = inLine[inCurCol]
253
+				if inChar in set('\r\n'):
254
+					inCurCol += 1
255
+				elif inParseState == 1:
256
+					inCurCol += 1
257
+					if inChar == '':
258
+						inCellState = self.flip_cell_state(inCellState, 0x01)
259
+					elif inChar == '':
260
+						inParseState = 2
261
+					elif inChar == '':
262
+						inCellState = self.flip_cell_state(inCellState, 0x02)
263
+					elif inChar == '':
264
+						inCellState |= 0x00
265
+						inCurColours = (15, 1)
266
+					elif inChar == '':
267
+						inCurColours = (inCurColours[1], inCurColours[0])
268
+					elif inChar == '':
269
+						inCellState = self.flip_cell_state(inCellState, 0x04)
270
+					else:
271
+						inRowCols += 1
272
+						outMap[inCurRow].append([*inCurColours, inCellState, inChar])
273
+				elif inParseState == 2 or inParseState == 3:
274
+					if inChar == ',' and inParseState == 2:
275
+						if (inCurCol + 1) < inMaxCol and not inLine[inCurCol + 1] in set('0123456789'):
276
+							inCurColours = self.parse_char(inCurColourSpec, inCurColours)
277
+							inCurColourDigits = 0; inCurColourSpec = '';
278
+							inParseState = 1
279
+						else:
280
+							inCurCol += 1
281
+							inCurColourDigits = 0; inCurColourSpec += inChar;
282
+							inParseState = 3
283
+					elif inChar in set('0123456789') and inCurColourDigits == 0:
284
+						inCurCol += 1
285
+						inCurColourDigits += 1; inCurColourSpec += inChar;
286
+					elif inChar in set('0123456789') and inCurColourDigits == 1 and inCurColourSpec[-1] == '0':
287
+						inCurCol += 1
288
+						inCurColourDigits += 1; inCurColourSpec += inChar;
289
+					elif inChar in set('012345') and inCurColourDigits == 1 and inCurColourSpec[-1] == '1':
290
+						inCurCol += 1
291
+						inCurColourDigits += 1; inCurColourSpec += inChar;
292
+					else:
293
+						inCurColours = self.parse_char(inCurColourSpec, inCurColours)
294
+						inCurColourDigits = 0; inCurColourSpec = '';
295
+						inParseState = 1
296
+			inMaxCols = max(inMaxCols, inRowCols)
297
+			inLine = self.inFile.readline()
298
+		inSize[0] = inMaxCols;
299
+		self.inSize = inSize;
300
+		self.outMap = outMap;
301
+		self.inFile.close()
302
diff --git a/scroll/core/mircart/mircart.py b/scroll/core/mircart/mircart.py
303
new file mode 100644
304
index 0000000..38d0af7
305
--- /dev/null
306
+++ b/scroll/core/mircart/mircart.py
307
@@ -0,0 +1,216 @@
308
+#!/usr/bin/env python
309
+# -*- coding: utf-8 -*-
310
+# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll))
311
+# mircart.py
312
+
313
+from PIL import Image, ImageDraw, ImageFont
314
+
315
+def ansi(input_file):
316
+	ansi_colors = [97,30,94,32,91,31,35,33,93,92,36,96,34,95,90,37]
317
+	canvasStore = canvas.(input_file)
318
+	inMap = canvasStore.outMap.copy(); del canvasStore;
319
+	with open(input_file, 'w+') as output_file:
320
+		for inCurRow in range(len(inMap)):
321
+			lastAttribs = canvas._CellState.CS_NONE
322
+			lastColours = None
323
+			for inCurCol in range(len(inMap[inCurRow])):
324
+				inCurCell = inMap[inCurRow][inCurCol]
325
+				if lastAttribs != inCurCell[2]:
326
+					if inCurCell[2] & canvas._CellState.CS_BOLD:
327
+						print('\u001b[1m', end='', file=output_file)
328
+					if inCurCell[2] & canvas._CellState.CS_UNDERLINE:
329
+						print('\u001b[4m', end='', file=output_file)
330
+					lastAttribs = inCurCell[2]
331
+				if lastColours == None or lastColours != inCurCell[:2]:
332
+					ansiBg = ansi_colors[int(inCurCell[1])] + 10
333
+					ansiFg = ansi_colors[int(inCurCell[0])]
334
+					print('\u001b[{:02d}m\u001b[{:02d}m{}'.format(ansiBg, ansiFg, inCurCell[3]), end='', file=output_file)
335
+					lastColours = inCurCell[:2]
336
+				else:
337
+					print(inCurCell[3], end='', file=output_file)
338
+			print('\u001b[0m\n', end='', file=output_file)
339
+
340
+def canonicalise(input_file):
341
+	canvasStore = canvas.(input_file)
342
+	inMap = canvasStore.outMap.copy(); del canvasStore;
343
+	with open(input_file, 'w+') as output_file:
344
+		for inCurRow in range(len(inMap)):
345
+			lastAttribs = canvas._CellState.CS_NONE
346
+			lastColours = None
347
+			for inCurCol in range(len(inMap[inCurRow])):
348
+				inCurCell = inMap[inCurRow][inCurCol]
349
+				if lastAttribs != inCurCell[2]:
350
+					if inCurCell[2] & canvas._CellState.CS_BOLD:
351
+						print('\u0002', end='', file=output_file)
352
+					if inCurCell[2] & canvas._CellState.CS_UNDERLINE:
353
+						print('\u001f', end='', file=output_file)
354
+					lastAttribs = inCurCell[2]
355
+				if lastColours == None or lastColours != inCurCell[:2]:
356
+					print('\u0003{:02d},{:02d}{}'.format(*inCurCell[:2], inCurCell[3]), end='', file=output_file)
357
+					lastColours = inCurCell[:2]
358
+				else:
359
+					print(inCurCell[3], end='', file=output_file)
360
+			print('\n', end='', file=output_file)#!/usr/bin/env python3
361
+
362
+class canvas():
363
+	def flip_cell_state(self, cellState, bit):
364
+		if cellState & bit:
365
+			return cellState & ~bit
366
+		else:
367
+			return cellState | bit
368
+
369
+	def parse_char(self, colourSpec, curColours):
370
+		if len(colourSpec) > 0:
371
+			colourSpec = colourSpec.split(',')
372
+			if len(colourSpec) == 2 and len(colourSpec[1]) > 0:
373
+				return (int(colourSpec[0] or curColours[0]), int(colourSpec[1]))
374
+			elif len(colourSpec) == 1 or len(colourSpec[1]) == 0:
375
+				return (int(colourSpec[0]), curColours[1])
376
+		else:
377
+			return (15, 1)
378
+
379
+	def importTextFile(self, pathName):
380
+		self.inFile = open(pathName, 'r', encoding='utf-8')
381
+		self.inSize = self.outMap = None;
382
+		inCurColourSpec = ''; inCurRow = -1;
383
+		inLine = self.inFile.readline()
384
+		inSize = [0, 0]; outMap = []; inMaxCols = 0;
385
+		while inLine:
386
+			inCellState = 0x00
387
+			inParseState = 1
388
+			inCurCol = 0; inMaxCol = len(inLine);
389
+			inCurColourDigits = 0; inCurColours = (15, 1); inCurColourSpec = '';
390
+			inCurRow += 1; outMap.append([]); inRowCols = 0; inSize[1] += 1;
391
+			while inCurCol < inMaxCol:
392
+				inChar = inLine[inCurCol]
393
+				if inChar in set('\r\n'):
394
+					inCurCol += 1
395
+				elif inParseState == 1:
396
+					inCurCol += 1
397
+					if inChar == '':
398
+						inCellState = self.flip_cell_state(inCellState, 0x01)
399
+					elif inChar == '':
400
+						inParseState = 2
401
+					elif inChar == '':
402
+						inCellState = self.flip_cell_state(inCellState, 0x02)
403
+					elif inChar == '':
404
+						inCellState |= 0x00
405
+						inCurColours = (15, 1)
406
+					elif inChar == '':
407
+						inCurColours = (inCurColours[1], inCurColours[0])
408
+					elif inChar == '':
409
+						inCellState = self.flip_cell_state(inCellState, 0x04)
410
+					else:
411
+						inRowCols += 1
412
+						outMap[inCurRow].append([*inCurColours, inCellState, inChar])
413
+				elif inParseState == 2 or inParseState == 3:
414
+					if inChar == ',' and inParseState == 2:
415
+						if (inCurCol + 1) < inMaxCol and not inLine[inCurCol + 1] in set('0123456789'):
416
+							inCurColours = self.parse_char(inCurColourSpec, inCurColours)
417
+							inCurColourDigits = 0; inCurColourSpec = '';
418
+							inParseState = 1
419
+						else:
420
+							inCurCol += 1
421
+							inCurColourDigits = 0; inCurColourSpec += inChar;
422
+							inParseState = 3
423
+					elif inChar in set('0123456789') and inCurColourDigits == 0:
424
+						inCurCol += 1
425
+						inCurColourDigits += 1; inCurColourSpec += inChar;
426
+					elif inChar in set('0123456789') and inCurColourDigits == 1 and inCurColourSpec[-1] == '0':
427
+						inCurCol += 1
428
+						inCurColourDigits += 1; inCurColourSpec += inChar;
429
+					elif inChar in set('012345') and inCurColourDigits == 1 and inCurColourSpec[-1] == '1':
430
+						inCurCol += 1
431
+						inCurColourDigits += 1; inCurColourSpec += inChar;
432
+					else:
433
+						inCurColours = self.parse_char(inCurColourSpec, inCurColours)
434
+						inCurColourDigits = 0; inCurColourSpec = '';
435
+						inParseState = 1
436
+			inMaxCols = max(inMaxCols, inRowCols)
437
+			inLine = self.inFile.readline()
438
+		inSize[0] = inMaxCols;
439
+		self.inSize = inSize;
440
+		self.outMap = outMap;
441
+		self.inFile.close()
442
+
443
+# canvasStore = canvas.(inFile=argv[1])
444
+# MiRCARTToPngFile(canvasStore.outMap, *argv[3:]).export(argv[2])
445
+class png:
446
+    inFile = inFromTextFile = None
447
+    outFontFilePath = outFontSize = None
448
+    _ColourMapBold   = [[255,255,255],[85,85,85],[85,85,255],[85,255,85],[255,85,85],[255,85,85],[255,85,255],[255,255,85],[255,255,85],[85,255,85],[85,255,255],[85,255,255],[85,85,255],[255,85,255],[85,85,85],[255,255,255]]
449
+    _ColourMapNormal = [[255,255,255],[0,0,0],[0,0,187],[0,187,0],[255,85,85],[187,0,0],[187,0,187],[187,187,0],[255,255,85],[85,255,85],[0,187,187],[85,255,255],[85,85,255],[255,85,255],[85,85,85],[187,187,187]]
450
+
451
+    def __init__(self, inCanvasMap, fontFilePath='DejaVuSansMono.ttf', fontSize=11):
452
+        self.inCanvasMap = inCanvasMap
453
+        self.outFontFilePath = fontFilePath; self.outFontSize = int(fontSize);
454
+        self.outImgFont = ImageFont.truetype(self.outFontFilePath, self.outFontSize)
455
+        self.outImgFontSize = [*self.outImgFont.getsize(' ')]
456
+        self.outImgFontSize[1] += 3
457
+
458
+    def _drawUnderLine(self, curPos, fontSize, imgDraw, fillColour):
459
+        imgDraw.line(xy=(curPos[0], curPos[1] + (fontSize[1] - 2), curPos[0] + fontSize[0], curPos[1] + (fontSize[1] - 2)), fill=fillColour)
460
+
461
+    def export(self, output_file):
462
+        inSize = (len(self.inCanvasMap[0]), len(self.inCanvasMap))
463
+        outSize = [a*b for a,b in zip(inSize, self.outImgFontSize)]
464
+        outCurPos = [0, 0]
465
+        outImg = Image.new('RGBA', outSize, (*self. _ColourMapNormal[1], 255))
466
+        outImgDraw = ImageDraw.Draw(outImg)
467
+        for inCurRow in range(len(self.inCanvasMap)):
468
+            for inCurCol in range(len(self.inCanvasMap[inCurRow])):
469
+                inCurCell = self.inCanvasMap[inCurRow][inCurCol]
470
+                outColours = [0, 0]
471
+                if inCurCell[2] & canvas._CellState.CS_BOLD:
472
+                    if inCurCell[3] != ' ':
473
+                        if inCurCell[3] == '█':
474
+                            outColours[1] = self._ColourMapNormal[inCurCell[0]]
475
+                        else:
476
+                            outColours[0] = self._ColourMapBold[inCurCell[0]]
477
+                            outColours[1] = self._ColourMapNormal[inCurCell[1]]
478
+                    else:
479
+                        outColours[1] = self._ColourMapNormal[inCurCell[1]]
480
+                else:
481
+                    if inCurCell[3] != ' ':
482
+                        if inCurCell[3] == '█':
483
+                            outColours[1] = self._ColourMapNormal[inCurCell[0]]
484
+                        else:
485
+                            outColours[0] = self._ColourMapNormal[inCurCell[0]]
486
+                            outColours[1] = self._ColourMapNormal[inCurCell[1]]
487
+                    else:
488
+                        outColours[1] = self._ColourMapNormal[inCurCell[1]]
489
+                outImgDraw.rectangle((*outCurPos, outCurPos[0] + self.outImgFontSize[0], outCurPos[1] + self.outImgFontSize[1]), fill=(*outColours[1], 255))
490
+                if not inCurCell[3] in ' █' and outColours[0] != outColours[1]:
491
+                    outImgDraw.text(outCurPos, inCurCell[3], (*outColours[0], 255), self.outImgFont)
492
+                if inCurCell[2] & canvas._CellState.CS_UNDERLINE:
493
+                    outColours[0] = self._ColourMapNormal[inCurCell[0]]
494
+                    self._drawUnderLine(outCurPos, self.outImgFontSize, outImgDraw, (*outColours[0], 255))
495
+                outCurPos[0] += self.outImgFontSize[0]
496
+            outCurPos[0] = 0
497
+            outCurPos[1] += self.outImgFontSize[1]
498
+        outImg.save(output_file)
499
+
500
+def reduce(input_file):
501
+	canvasStore = canvas.(input_file)
502
+	inMap = canvasStore.outMap.copy(); del canvasStore;
503
+	with open(input_file, 'w+') as output_file:
504
+		for inCurRow in range(len(inMap)):
505
+			lastAttribs = canvas._CellState.CS_NONE
506
+			lastColours = None
507
+			for inCurCol in range(len(inMap[inCurRow])):
508
+				inCurCell = inMap[inCurRow][inCurCol]
509
+				if lastAttribs != inCurCell[2]:
510
+					if inCurCell[2] & canvas._CellState.CS_BOLD:
511
+						print('\u0002', end='', file=output_file)
512
+					if inCurCell[2] & canvas._CellState.CS_UNDERLINE:
513
+						print('\u001f', end='', file=output_file)
514
+					lastAttribs = inCurCell[2]
515
+				if lastColours == None or (lastColours[0] != inCurCell[:2][0] and lastColours[1] != inCurCell[:2][1]):
516
+					print('\u0003{:d},{:d}{}'.format(*inCurCell[:2], inCurCell[3]), end='', file=output_file)
517
+					lastColours = inCurCell[:2]
518
+				elif lastColours[1] == inCurCell[:2][1] and lastColours[0] != inCurCell[:2][0]:
519
+					print('\u0003{:d}{}'.format(inCurCell[:2][0], inCurCell[3]), end='', file=output_file)
520
+					lastColours[0] = inCurCell[:2][0]
521
+				else:
522
+					print(inCurCell[3], end='', file=output_file)
523
+			print('\n', end='', file=output_file)
524
diff --git a/scroll/core/mircart/png.py b/scroll/core/mircart/png.py
525
new file mode 100644
526
index 0000000..b921f33
527
--- /dev/null
528
+++ b/scroll/core/mircart/png.py
529
@@ -0,0 +1,60 @@
530
+#!/usr/bin/env python
531
+import canvas
532
+from PIL import Image, ImageDraw, ImageFont
533
+
534
+# canvasStore = canvas.store(inFile=argv[1])
535
+# MiRCARTToPngFile(canvasStore.outMap, *argv[3:]).export(argv[2])
536
+class png:
537
+    inFile = inFromTextFile = None
538
+    outFontFilePath = outFontSize = None
539
+    _ColourMapBold   = [[255,255,255],[85,85,85],[85,85,255],[85,255,85],[255,85,85],[255,85,85],[255,85,255],[255,255,85],[255,255,85],[85,255,85],[85,255,255],[85,255,255],[85,85,255],[255,85,255],[85,85,85],[255,255,255]]
540
+    _ColourMapNormal = [[255,255,255],[0,0,0],[0,0,187],[0,187,0],[255,85,85],[187,0,0],[187,0,187],[187,187,0],[255,255,85],[85,255,85],[0,187,187],[85,255,255],[85,85,255],[255,85,255],[85,85,85],[187,187,187]]
541
+
542
+    def __init__(self, inCanvasMap, fontFilePath='DejaVuSansMono.ttf', fontSize=11):
543
+        self.inCanvasMap = inCanvasMap
544
+        self.outFontFilePath = fontFilePath; self.outFontSize = int(fontSize);
545
+        self.outImgFont = ImageFont.truetype(self.outFontFilePath, self.outFontSize)
546
+        self.outImgFontSize = [*self.outImgFont.getsize(' ')]
547
+        self.outImgFontSize[1] += 3
548
+
549
+    def _drawUnderLine(self, curPos, fontSize, imgDraw, fillColour):
550
+        imgDraw.line(xy=(curPos[0], curPos[1] + (fontSize[1] - 2), curPos[0] + fontSize[0], curPos[1] + (fontSize[1] - 2)), fill=fillColour)
551
+
552
+    def export(self, output_file):
553
+        inSize = (len(self.inCanvasMap[0]), len(self.inCanvasMap))
554
+        outSize = [a*b for a,b in zip(inSize, self.outImgFontSize)]
555
+        outCurPos = [0, 0]
556
+        outImg = Image.new('RGBA', outSize, (*self. _ColourMapNormal[1], 255))
557
+        outImgDraw = ImageDraw.Draw(outImg)
558
+        for inCurRow in range(len(self.inCanvasMap)):
559
+            for inCurCol in range(len(self.inCanvasMap[inCurRow])):
560
+                inCurCell = self.inCanvasMap[inCurRow][inCurCol]
561
+                outColours = [0, 0]
562
+                if inCurCell[2] & canvas.store._CellState.CS_BOLD:
563
+                    if inCurCell[3] != ' ':
564
+                        if inCurCell[3] == '█':
565
+                            outColours[1] = self._ColourMapNormal[inCurCell[0]]
566
+                        else:
567
+                            outColours[0] = self._ColourMapBold[inCurCell[0]]
568
+                            outColours[1] = self._ColourMapNormal[inCurCell[1]]
569
+                    else:
570
+                        outColours[1] = self._ColourMapNormal[inCurCell[1]]
571
+                else:
572
+                    if inCurCell[3] != ' ':
573
+                        if inCurCell[3] == '█':
574
+                            outColours[1] = self._ColourMapNormal[inCurCell[0]]
575
+                        else:
576
+                            outColours[0] = self._ColourMapNormal[inCurCell[0]]
577
+                            outColours[1] = self._ColourMapNormal[inCurCell[1]]
578
+                    else:
579
+                        outColours[1] = self._ColourMapNormal[inCurCell[1]]
580
+                outImgDraw.rectangle((*outCurPos, outCurPos[0] + self.outImgFontSize[0], outCurPos[1] + self.outImgFontSize[1]), fill=(*outColours[1], 255))
581
+                if not inCurCell[3] in ' █' and outColours[0] != outColours[1]:
582
+                    outImgDraw.text(outCurPos, inCurCell[3], (*outColours[0], 255), self.outImgFont)
583
+                if inCurCell[2] & canvas.store._CellState.CS_UNDERLINE:
584
+                    outColours[0] = self._ColourMapNormal[inCurCell[0]]
585
+                    self._drawUnderLine(outCurPos, self.outImgFontSize, outImgDraw, (*outColours[0], 255))
586
+                outCurPos[0] += self.outImgFontSize[0]
587
+            outCurPos[0] = 0
588
+            outCurPos[1] += self.outImgFontSize[1]
589
+        outImg.save(output_file)
590
diff --git a/scroll/scroll.py b/scroll/scroll.py
591
index e0265f1..bc0a3c3 100644
592
--- a/scroll/scroll.py
593
+++ b/scroll/scroll.py
594
@@ -1,5 +1,5 @@
595
 #!/usr/bin/env python
596
-# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
597
+# Scroll IRC Art Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
598
 # scroll.py
599
 
600
 import os