Merge lp:~marco-gallotta/ibid/ascii-501638 into lp:~ibid-core/ibid/old-trunk-1.6
- ascii-501638
- Merge into old-trunk-1.6
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 | ||||
Related bugs: |
|
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 |
Commit message
Description of the change
Michael Gorven (mgorven) wrote : | # |
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.addrespon
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.addrespon
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.
> event.addrespon
Use the new multiline API
> process = subprocess.
> shell=True, stdout=
eek, that's begging for a shell injection. Rather use the arg-list-style popen, with shell=False if you can.
- 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
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.addrespon
>
> 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.addrespon
>
> 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.
> > event.addrespon
>
> Use the new multiline API
>
> > process = subprocess.
> height, image),
> > shell=True, stdout=
>
> eek, that's begging for a shell injection. Rather use the arg-list-style
> popen, with shell=False if you can.
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\
> event.addrespon
No, not quite like that. You still do a substitution, but it does it for you:
event.addrespon
> event.addrespon
I suspect that'll throw a unicode warning.
> fonts_zip = Option('fonts_zip', 'Zip file containing figlet fonts', '/tmp/pyfiglet-
We can't merge with that default. I'd make the path relative to ibid.config.base (the botdir).
I.e. 'data/figlet-
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"
Stefano Rivera (stefanor) wrote : | # |
> event.addrespon
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
Stefano Rivera (stefanor) wrote : | # |
figlet also throws a unicode warning
- 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
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\
>
> > event.addrespon
>
> No, not quite like that. You still do a substitution, but it does it for you:
>
> event.addrespon
>
> > event.addrespon
>
> I suspect that'll throw a unicode warning.
>
> > fonts_zip = Option('fonts_zip', 'Zip file containing figlet fonts',
> '/tmp/pyfiglet-
>
> We can't merge with that default. I'd make the path relative to
> ibid.config.base (the botdir).
> I.e. 'data/figlet-
>
> 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"
marcog (marco-gallotta) wrote : | # |
> > event.addrespon
>
> 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).
- 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
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.
Stefano Rivera (stefanor) wrote : | # |
> max_width = IntOption(
> max_height = IntOption(
^ Some typo there
But otherwise I approve
marcog (marco-gallotta) wrote : | # |
> > max_width = IntOption(
> > max_height = IntOption(
>
> ^ Some typo there
Well spotted :) fixed!
- 818. By Marco Gallotta <marco@me>
-
Fix typo (s/width/height/)
Jonathan Hitchcock (vhata) : | # |
Preview Diff
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) |
I'd prefer the non-stdlib imports to be grouped separately. Also not mad about 0.4/fonts. zip' default, but as discussed on IRC the pyfiglet
the '/tmp/pyfiglet-
package is kak and there's not really a good default.