Merge lp:~marco-gallotta/ibid/ascii-501638 into lp:~ibid-core/ibid/old-trunk-1.6

Proposed by marcog
Status: Merged
Approved by: Jonathan Hitchcock
Approved revision: not available
Merged at revision: 820
Proposed branch: lp:~marco-gallotta/ibid/ascii-501638
Merge into: lp:~ibid-core/ibid/old-trunk-1.6
Diff against target: 165 lines (+161/-0)
1 file modified
ibid/plugins/ascii.py (+161/-0)
To merge this branch: bzr merge lp:~marco-gallotta/ibid/ascii-501638
Reviewer Review Type Date Requested Status
Jonathan Hitchcock Approve
Stefano Rivera Approve
Michael Gorven Approve
marcog (community) Needs Resubmitting
Review via email: mp+16675@code.launchpad.net
To post a comment you must log in.
lp:~marco-gallotta/ibid/ascii-501638 updated
810. By Marco Gallotta <marco@me>

Fix typo; change 'figlet list fonts

811. By Marco Gallotta <marco@me>

Fix caca rendering by writing directly to file rather than going through Image lib

Revision history for this message
Michael Gorven (mgorven) wrote :

I'd prefer the non-stdlib imports to be grouped separately. Also not mad about
the '/tmp/pyfiglet-0.4/fonts.zip' default, but as discussed on IRC the pyfiglet
package is kak and there's not really a good default.

Revision history for this message
Stefano Rivera (stefanor) wrote :

good imports ordering is:
stdlib

otherlibs

ibidlibs

code

Look at other plugins that call out to see how we handle binaries and paths. i.e. the stuff in setup that aborts loading if the binary isn't in $PATH.
We're not entirely happy with it, but it's the current standard.

> lib = "aa" if colour is None else "caca"

Conditional statements are only in Python >= 2.5, we support python >= 2.4.
Applies elsewhere too

> try ... except: event.addresponse(u'Cannot fetch %s' % url)

Is that totally necessary? We try to catch as few exceptions as possible and when we do catch them, return a suitable error. As soon as you catch an exception, the bot won't save a traceback so the devs / owners get no debugging information.

> event.addresponse(u'Sorry, don\'t understand lib %s' % lib)

Firstly, save a character by using double-quotes.
Second, do ", lib" rather than "% lib". Addresponse will support translation in the future. This applies to all your addresponses
Third, it complains about an internal variable (lib) rather than what the user sees ("colour")

> height = 30

That's enormous, maybe default to lower. I think it should have a configurable maximum height to limit spam.

> for line in screen.render().split('\n'):
> event.addresponse(unicode(line))

Use the new multiline API

> process = subprocess.Popen('%s -f irc %s %s %s' % (self.img2txt_bin, width, height, image),
> shell=True, stdout=subprocess.PIPE)

eek, that's begging for a shell injection. Rather use the arg-list-style popen, with shell=False if you can.

review: Needs Fixing
lp:~marco-gallotta/ibid/ascii-501638 updated
812. By Marco Gallotta <marco@me>

use deaddressed for figlet to leave trailing punctuation

813. By Marco Gallotta <marco@me>

Update to handle api change for multiline responses

814. By Marco Gallotta <marco@me>

Handle unfound binary; remove conditional statements; remove some error messages; redo formatting of arg-based responses; reduce default height and make it configurable; remove potential shell injection

Revision history for this message
marcog (marco-gallotta) wrote :

Okay, I made changes based on all the comments below. Hope it's good.

> good imports ordering is:
> stdlib
>
> otherlibs
>
> ibidlibs
>
> code
>
> Look at other plugins that call out to see how we handle binaries and paths.
> i.e. the stuff in setup that aborts loading if the binary isn't in $PATH.
> We're not entirely happy with it, but it's the current standard.
>
> > lib = "aa" if colour is None else "caca"
>
> Conditional statements are only in Python >= 2.5, we support python >= 2.4.
> Applies elsewhere too
>
> > try ... except: event.addresponse(u'Cannot fetch %s' % url)
>
> Is that totally necessary? We try to catch as few exceptions as possible and
> when we do catch them, return a suitable error. As soon as you catch an
> exception, the bot won't save a traceback so the devs / owners get no
> debugging information.
>
> > event.addresponse(u'Sorry, don\'t understand lib %s' % lib)
>
> Firstly, save a character by using double-quotes.
> Second, do ", lib" rather than "% lib". Addresponse will support translation
> in the future. This applies to all your addresponses
> Third, it complains about an internal variable (lib) rather than what the user
> sees ("colour")
>
> > height = 30
>
> That's enormous, maybe default to lower. I think it should have a configurable
> maximum height to limit spam.
>
> > for line in screen.render().split('\n'):
> > event.addresponse(unicode(line))
>
> Use the new multiline API
>
> > process = subprocess.Popen('%s -f irc %s %s %s' % (self.img2txt_bin, width,
> height, image),
> > shell=True, stdout=subprocess.PIPE)
>
> eek, that's begging for a shell injection. Rather use the arg-list-style
> popen, with shell=False if you can.

review: Needs Resubmitting
Revision history for this message
Stefano Rivera (stefanor) wrote :

> === modified file 'ibid.ini'

Somehow you modified ibid.ini. (probably when merging from trunk).

> (?:\s+(in colou?r))?

You only compare this string to none, so you can say r'(\s+in\s+colou?r)?'

> event.addresponse(u'File too large (limit is', self.max_size, 'KiB)')

No, not quite like that. You still do a substitution, but it does it for you:

event.addresponse(u'File too large (limit is %i KiB)', self.max_size)

> event.addresponse(response.replace('\r', ''), address=False, conflate=False)

I suspect that'll throw a unicode warning.

> fonts_zip = Option('fonts_zip', 'Zip file containing figlet fonts', '/tmp/pyfiglet-0.4/fonts.zip')

We can't merge with that default. I'd make the path relative to ibid.config.base (the botdir).
I.e. 'data/figlet-fonts.zip'

Overall, I'd still like some limits. Maximum width & height of all possible ascii art output.
If it sees that it's making something too big, it should just return something like
"I'm sorry, I can't draw ascii graffiti that big"

review: Needs Fixing
Revision history for this message
Stefano Rivera (stefanor) wrote :

> event.addresponse(response.replace('\r', ''), address=False, conflate=False)

On my machine it doesn't include CRs (\r). BTW the Debian/Ubuntu package is caca-utils you can mention that in the doc at the top

Revision history for this message
Stefano Rivera (stefanor) wrote :

figlet also throws a unicode warning

lp:~marco-gallotta/ibid/ascii-501638 updated
815. By Marco Gallotta <marco@me>

Touch up minor issues (unicode responses, fonts_zip default)

816. By Marco Gallotta <marco@me>

Improved resizing that maintains aspect ratio and implements size limitations; Syntax for size changed from 10x20 to width 10 height 20 to allow only one dimension to be set

Revision history for this message
marcog (marco-gallotta) wrote :

All these issues should be resolved in the latest commits.

I did a bit of refactoring on the resizing:
* input syntax changes from [<width>x<height>] to [width <width>] [height <height>], so that just one dimension can be set and the other determined from aspect ratio.
* I've made the assumed font size (used to work out the dimensions in text form) configurable; I think the defaults are quite reasonable but feel free to suggest alternatives.
* lowered the default height and selected what I feel are sane defaults for max_{width,height}.
* if the aspect ratio would make the calculated dimension too large, it squashes the image rather than throwing an error.
* if the supplied dimensions are too large, throw an error.

> > === modified file 'ibid.ini'
>
> Somehow you modified ibid.ini. (probably when merging from trunk).
>
> > (?:\s+(in colou?r))?
>
> You only compare this string to none, so you can say r'(\s+in\s+colou?r)?'
>
> > event.addresponse(u'File too large (limit is', self.max_size, 'KiB)')
>
> No, not quite like that. You still do a substitution, but it does it for you:
>
> event.addresponse(u'File too large (limit is %i KiB)', self.max_size)
>
> > event.addresponse(response.replace('\r', ''), address=False, conflate=False)
>
> I suspect that'll throw a unicode warning.
>
> > fonts_zip = Option('fonts_zip', 'Zip file containing figlet fonts',
> '/tmp/pyfiglet-0.4/fonts.zip')
>
> We can't merge with that default. I'd make the path relative to
> ibid.config.base (the botdir).
> I.e. 'data/figlet-fonts.zip'
>
> Overall, I'd still like some limits. Maximum width & height of all possible
> ascii art output.
> If it sees that it's making something too big, it should just return something
> like
> "I'm sorry, I can't draw ascii graffiti that big"

review: Needs Resubmitting
Revision history for this message
marcog (marco-gallotta) wrote :

> > event.addresponse(response.replace('\r', ''), address=False, conflate=False)
>
> On my machine it doesn't include CRs (\r). BTW the Debian/Ubuntu package is
> caca-utils you can mention that in the doc at the top

It definitely includes them on my box. Either way, it's safer to strip them cause if they aren't there it just won't do anything. I've mentioned the debian package (thanks for that).

lp:~marco-gallotta/ibid/ascii-501638 updated
817. By Marco Gallotta <marco@me>

Rather than squishing or returning an error, just shrink the size of the image and let the user know we drew it smaller

Revision history for this message
marcog (marco-gallotta) wrote :

Changed the handling of requests that are too large after discussion with Max on IRC. Instead of squishing the aspect ratio / returning an error, it now draws it as large as it can at the original aspect ratio while remaining within the max_{width,height} and tells the user we drew it smaller than asked for.

Revision history for this message
Michael Gorven (mgorven) wrote :

 review approve

review: Approve
Revision history for this message
Stefano Rivera (stefanor) wrote :

> max_width = IntOption('max_width', 'Maximum width for ascii output', 60)
> max_height = IntOption('max_width', 'Maximum width for ascii output', 15)

^ Some typo there

But otherwise I approve

review: Approve
Revision history for this message
marcog (marco-gallotta) wrote :

> > max_width = IntOption('max_width', 'Maximum width for ascii output', 60)
> > max_height = IntOption('max_width', 'Maximum width for ascii output', 15)
>
> ^ Some typo there

Well spotted :) fixed!

lp:~marco-gallotta/ibid/ascii-501638 updated
818. By Marco Gallotta <marco@me>

Fix typo (s/width/height/)

Revision history for this message
Jonathan Hitchcock (vhata) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'ibid/plugins/ascii.py'
2--- ibid/plugins/ascii.py 1970-01-01 00:00:00 +0000
3+++ ibid/plugins/ascii.py 2009-12-31 17:46:13 +0000
4@@ -0,0 +1,161 @@
5+from cStringIO import StringIO
6+import Image
7+from os import remove
8+import os.path
9+import subprocess
10+from sys import stderr
11+from tempfile import mkstemp
12+from urllib2 import urlopen
13+from zipfile import ZipFile
14+
15+from aalib import AsciiScreen
16+from pyfiglet import Figlet
17+
18+from ibid.config import Option, IntOption
19+from ibid.plugins import Processor, match
20+from ibid.utils import file_in_path
21+
22+"""
23+Dependencies:
24+ * libaa (Ubuntu's libaa1-dev package)
25+ * libcaca (Ubuntu's caca-utils package)
26+ * Imlib2 (Ubuntu's libimlib2-dev package)
27+ * pyfiglet (http://sourceforge.net/projects/pyfiglet/)
28+"""
29+
30+help = { 'draw' : u'Retrieve images from the web and render them in ascii-art.',
31+ 'figlet': u'Render text in ascii-art using figlet.' }
32+class DrawImage(Processor):
33+ u"""draw <url> [in colour] [width <width>] [height <height>]"""
34+ feature = 'draw'
35+
36+ max_filesize = IntOption('max_filesize', 'Only request this many KiB', 200)
37+ def_height = IntOption('def_height', 'Default height for libaa output', 10)
38+ max_width = IntOption('max_width', 'Maximum width for ascii output', 60)
39+ max_height = IntOption('max_height', 'Maximum height for ascii output', 15)
40+ font_width = IntOption('font_width', 'Font width assumed for output', 6)
41+ font_height = IntOption('font_height', 'Font height assumed for output', 10)
42+ img2txt_bin = Option('img2txt_bin', 'libcaca img2txt binary to use', 'img2txt')
43+
44+ def setup(self):
45+ if not file_in_path(self.img2txt_bin):
46+ raise Exception('Cannot locate img2txt executable')
47+
48+ @match(r'^draw\s+(\S+\.\S+)(\s+in\s+colou?r)?(?:\s+w(?:idth)?\s+(\d+))?(?:\s+h(?:eight)\s+(\d+))?$')
49+ def draw(self, event, url, colour, width, height):
50+ f = urlopen(url)
51+
52+ filesize = int(f.info().getheaders('Content-Length')[0])
53+ if filesize > self.max_filesize * 1024:
54+ event.addresponse(u'File too large (limit is %i KiB)', self.max_filesize)
55+ return
56+
57+ try:
58+ ext = os.path.splitext(url)[1]
59+ image = mkstemp(suffix=ext)[1]
60+ file = open(image, 'w')
61+ file.write(f.read())
62+ file.close()
63+
64+ try:
65+ img = Image.open(StringIO(open(image, 'r').read())).convert('L')
66+ except:
67+ event.addresponse(u'Cannot understand image format')
68+ return
69+ input_width, input_height = img.size[0], img.size[1]
70+
71+ set_size = width is not None or height is not None
72+ if width is None and height is None:
73+ height = self.def_height
74+ width = height * input_width * self.font_height / input_height / self.font_width
75+ elif width is None: # only height is set
76+ height = int(height)
77+ width = height * input_width * self.font_height / input_height / self.font_width
78+ elif height is None: # only width is set
79+ width = int(width)
80+ height = width * input_height * self.font_width / input_width / self.font_height
81+ else: # both width and height are set
82+ width, height = int(width), int(height)
83+
84+ smaller = False
85+ if width > self.max_width:
86+ width = self.max_width
87+ height = width * input_height * self.font_width / input_width / self.font_height
88+ smaller = True
89+ if height > self.max_height:
90+ height = self.max_height
91+ width = height * input_width * self.font_height / input_height / self.font_width
92+ smaller = True
93+
94+ if colour is None:
95+ self.draw_aa(event, image, width, height)
96+ else:
97+ self.draw_caca(event, image, width, height)
98+
99+ if set_size and smaller:
100+ event.addresponse(u'Sorry, I drew that smaller than you asked for')
101+ finally:
102+ remove(image)
103+
104+ def draw_aa(self, event, image, width, height):
105+ try:
106+ image = Image.open(StringIO(open(image, 'r').read())).convert('L')
107+ except:
108+ event.addresponse(u'Cannot understand image format')
109+ return
110+ screen = AsciiScreen(width=width, height=height)
111+ image = image.resize(screen.virtual_size)
112+ screen.put_image((0, 0), image)
113+ event.addresponse(unicode(screen.render()), address=False, conflate=False)
114+
115+ def draw_caca(self, event, image, width, height):
116+ from sys import stderr
117+ process = subprocess.Popen(
118+ [self.img2txt_bin, '-f', 'irc', '-W', str(width), '-H', str(height), image],
119+ shell=False, stdout=subprocess.PIPE)
120+ response, error = process.communicate()
121+ code = process.wait()
122+ if code == 0:
123+ event.addresponse(unicode(response.replace('\r', '')), address=False, conflate=False)
124+ else:
125+ event.addresponse(u'Sorry, cannot understand image format')
126+
127+class WriteFiglet(Processor):
128+ u"""figlet <text> [in <font>]
129+ list figlet fonts [from <index>]"""
130+ feature = 'figlet'
131+
132+ fonts_zip = Option('fonts_zip', 'Zip file containing figlet fonts', 'data/figlet-fonts.zip')
133+
134+ def __init__(self, name):
135+ Processor.__init__(self, name)
136+ zip = ZipFile(self.fonts_zip)
137+ self.fonts = sorted(map(lambda n: os.path.splitext(os.path.split(n)[1])[0], zip.namelist()))
138+
139+ @match(r'^list\s+figlet\s+fonts(?:\s+from\s+(\d+))?$')
140+ def list_fonts(self, event, index):
141+ if index is None:
142+ index = 0
143+ index = int(index)
144+ if index >= len(self.fonts):
145+ event.addresponse(u'I wish I had that many fonts installed')
146+ return
147+ event.addresponse(unicode(', '.join(self.fonts[int(index):])))
148+
149+ @match(r'^figlet\s+(.+?)(\s+in\s+(\S+))?$', 'deaddressed')
150+ def write(self, event, text, font_phrase, font):
151+ if font is not None and font not in self.fonts:
152+ text = '%s%s' % (text, font_phrase)
153+ font = None
154+ if font is None:
155+ font = 'slant'
156+ self._write(event, text, font)
157+
158+ def _write(self, event, text, font):
159+ figlet = Figlet(font=font, zipfile=self.fonts_zip)
160+ rendered = figlet.renderText(text).split('\n')
161+ while rendered and rendered[0].strip() == '':
162+ del rendered[0]
163+ while rendered and rendered[-1].strip() == '':
164+ del rendered[-1]
165+ event.addresponse(unicode('\n'.join(rendered)), address=False, conflate=False)

Subscribers

People subscribed via source and target branches