Skip to content

Commit a1b1f53

Browse files
committed
Added support to minify images.
1 parent af51591 commit a1b1f53

File tree

2 files changed

+199
-20
lines changed

2 files changed

+199
-20
lines changed

README.md

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ Example with a django project
1010

1111
During development I simply use django tags like *{% url ... %}* and *{% static ... %}* on my .js and .css files to define dynamic url to views and images of my project. At this point the .js and .css are placed into the templates directory of my app so I can *{% include ... %}* it to my .html templates and have the tags rendered to final urls. Of course the js and css will appear inline with the HTML content. For development I think this is fine.
1212

13-
On deploy you usually what all your javascript and styling to be in a external file.
13+
On deploy you usually want all your javascript and styling to be in a single external file.
1414

1515
Here is how this script can help you with that:
1616

1717
On my base.html template I usually have something like this:
1818

1919
```html
20+
<head>
21+
...
2022
{% if not debug %}
2123
<script src="{% static 'js/ceco.min.js' %}"></script>
2224
{% else %}
@@ -29,6 +31,8 @@ On my base.html template I usually have something like this:
2931
{% include 'message/message.js' %}
3032
</script>
3133
{% endif %}
34+
...
35+
</head>
3236
```
3337

3438
Then simply put this script on the same directory as manager.py. Edit the script to adjust your paths to settings.py and to every JS and CSS you whant to merge/minify. Like:
@@ -59,7 +63,7 @@ merger = {
5963
}
6064
```
6165

62-
Here we define a path to the django project and the settings.py file to use while rendering the template tags. *You only need to define those if you have tags inside .js or .css, if you don't then simply ignore both*
66+
Here we define a path to the django project and the settings.py file to use while rendering the template tags. *You only need to define those if you have tags inside .js or .css, if you don't then simply ignore both*.
6367

6468
In this case we have two blocks, *my js* and *my css*.
6569

@@ -92,8 +96,15 @@ $python manage.py collectstatic
9296
$touch /path/to/yourwsgi.wsgi
9397
```
9498

95-
Ok, this can change on your server but that the overall idea...
99+
Ok, this can change on your server but that's the overall idea...
96100

101+
Also you can use the option *--images* to optimise **all** *.PNG* and *.JPG* present on the directory tree starting where you executed this script (usualy your django project root). Like:
102+
103+
```bash
104+
$python jscssmin.py --images
105+
```
106+
107+
This will save a copy of all original images with *".original"* appended to its name and a smaller (hopefully), version will be saved with the original name. This script will ignore every file that already have a *.original* version present. To force this script to process every single file again use the option *--images-full*.
97108

98109
Non django project
99110
------------------
@@ -105,18 +116,32 @@ Using as a minifier module
105116

106117
In another python script:
107118
```python
108-
from jscssmin import jsMin, cssMin
119+
import jscssmin
109120

121+
#minify JS from text to file
110122
fulljstext = '<script>full code....</script>'
123+
jscssmin.jsMin(fulljstext, 'path/to/min/code.js')
124+
125+
#minify CSS from text to file
111126
fullcsstext = '<style>full styles...</style>'
127+
jscssmin.cssMin(fullcsstext, 'path/to/min/style.css')
128+
129+
#minify a JPG image
130+
jscssmin.jpgMin('path/to/image.jpg')
112131

113-
jsMin(fulljstext, 'path/to/min/code.js')
114-
cssMin(fullcsstext, 'path/to/min/style.css')
132+
#minify a PNG image
133+
jscssmin.pngMin('path/to/image.png')
115134
```
116135

136+
All four functions used above are only helper to call some online minifer api. See below. This script does not minify JS or CSS by itself, so you need to be online to execute this with success.
137+
117138
Minifier
118139
--------
119140

120-
This script uses the api provided by http://cssminifier.com and http://javascript-minifier.com/ to minify CSS and JS data. Thanks [@andychilton] for this api.
141+
This script uses some online api provided by [@andychilton] for all minify action:
142+
+ http://javascript-minifier.com
143+
+ http://cssminifier.com
144+
+ http://pngcrush.com
145+
+ http://jpgoptimiser.com
121146

122-
[@andychilton]: http://twitter.com/andychilton
147+
[@andychilton]: http://twitter.com/andychilton

jscssmin.py

Lines changed: 166 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
#minified version.
1616
#
1717
merger = {
18-
'config': "cecoe.settings",
18+
'config': "ceco.settings",
1919
'path': ("/home/transweb/apps_wsgi/ceco",
20-
#"more paths....",
20+
#"mode paths....",
2121
),
2222
'blocks': {'my js': {'static': ('static/js/ceco.js',
2323
'static/js/xvalidator.js',
@@ -31,19 +31,21 @@
3131
'my css': {'static': ("tasks/static/tasks/css/fixtypeahead.css",
3232
"message/static/message/css/message.css"
3333
),
34-
'full': 'static/css/ceco.css',
34+
'full': '',
3535
'cssmin': 'static/css/ceco.min.css'
3636
},
3737
},
3838
}
3939

40-
4140
#-------------------------------------------------------------------------
4241
import urllib2, urllib
42+
import os
43+
import sys
44+
import mimetypes
45+
import random
46+
import string
4347

4448
if merger.get('config'): #only imports django if we have a config file defined
45-
import os
46-
import sys
4749
import re
4850

4951
for p in merger['path']: sys.path.append(p)
@@ -59,19 +61,19 @@
5961
sys.exit(1)
6062

6163

62-
def _read(file):
64+
def _read(file, mode='r'):
6365
"""
6466
Read entire file content.
6567
"""
66-
with open(file, 'r') as fh:
68+
with open(file, mode) as fh:
6769
return fh.read()
6870

6971

70-
def _save(file, data):
72+
def _save(file, data, mode='w+'):
7173
"""
7274
Write all data to created file. Also overwrite previous file.
7375
"""
74-
with open(file, 'w+') as fh:
76+
with open(file, mode) as fh:
7577
fh.write(data)
7678

7779

@@ -154,6 +156,139 @@ def cssMin(data, file):
154156
return 0
155157

156158

159+
def jpgMin(file, force=False):
160+
"""
161+
Try to optimise a JPG file.
162+
163+
The original will be saved at the same place with '.original' appended to its name.
164+
165+
Once a .original exists the function will ignore this file unless force is True.
166+
"""
167+
if not os.path.isfile(file+'.original') or force:
168+
data = _read(file, 'rb')
169+
_save(file+'.original', data, 'w+b')
170+
print 'Optmising JPG {} - {:.2f}kB'.format(file, len(data)/1024.0),
171+
url = 'http://jpgoptimiser.com/optimise'
172+
parts, headers = encode_multipart({}, {'input': {'filename': 'wherever.jpg', 'content': data}})
173+
req = urllib2.Request(url, data=parts, headers=headers)
174+
try:
175+
f = urllib2.urlopen(req)
176+
response = f.read()
177+
f.close()
178+
print ' - {:.2f} - {:.1f}%'.format(len(response)/1024.0, 100.0*len(response)/len(data))
179+
_save(file, response, 'w+b')
180+
except:
181+
print 'Oops!! Failed :('
182+
return 1
183+
else:
184+
print 'Ignoring file: {}'.format(file)
185+
return 0
186+
187+
188+
def pngMin(file, force=False):
189+
"""
190+
Try to optimise a PNG file.
191+
192+
The original will be saved at the same place with '.original' appended to its name.
193+
194+
Once a .original exists the function will ignore this file unless force is True.
195+
"""
196+
if not os.path.isfile(file+'.original') or force:
197+
data = _read(file, 'rb')
198+
_save(file+'.original', data, 'w+b')
199+
print 'Crushing PNG {} - {:.2f}kB'.format(file, len(data)/1024.0),
200+
url = 'http://pngcrush.com/crush'
201+
parts, headers = encode_multipart({}, {'input': {'filename': 'wherever.jpg', 'content': data}})
202+
req = urllib2.Request(url, data=parts, headers=headers)
203+
try:
204+
f = urllib2.urlopen(req)
205+
response = f.read()
206+
f.close()
207+
print ' - {:.2f}kB - {:.1f}%'.format(len(response)/1024.0, 100.0*len(response)/len(data))
208+
_save(file, response, 'w+b')
209+
except:
210+
print 'Oops!! Failed :('
211+
return 1
212+
else:
213+
print 'Ignoring file: {}'.format(file)
214+
return 0
215+
216+
217+
#this codes cames from: http://code.activestate.com/recipes/578668-encode-multipart-form-data-for-uploading-files-via/
218+
def encode_multipart(fields, files, boundary=None):
219+
r"""Encode dict of form fields and dict of files as multipart/form-data.
220+
Return tuple of (body_string, headers_dict). Each value in files is a dict
221+
with required keys 'filename' and 'content', and optional 'mimetype' (if
222+
not specified, tries to guess mime type or uses 'application/octet-stream').
223+
224+
>>> body, headers = encode_multipart({'FIELD': 'VALUE'},
225+
... {'FILE': {'filename': 'F.TXT', 'content': 'CONTENT'}},
226+
... boundary='BOUNDARY')
227+
>>> print('\n'.join(repr(l) for l in body.split('\r\n')))
228+
'--BOUNDARY'
229+
'Content-Disposition: form-data; name="FIELD"'
230+
''
231+
'VALUE'
232+
'--BOUNDARY'
233+
'Content-Disposition: form-data; name="FILE"; filename="F.TXT"'
234+
'Content-Type: text/plain'
235+
''
236+
'CONTENT'
237+
'--BOUNDARY--'
238+
''
239+
>>> print(sorted(headers.items()))
240+
[('Content-Length', '193'), ('Content-Type', 'multipart/form-data; boundary=BOUNDARY')]
241+
>>> len(body)
242+
193
243+
"""
244+
def escape_quote(s):
245+
return s.replace('"', '\\"')
246+
247+
if boundary is None:
248+
boundary = ''.join(random.choice(string.digits + string.ascii_letters) for i in range(30))
249+
lines = []
250+
251+
for name, value in fields.items():
252+
lines.extend((
253+
'--{0}'.format(boundary),
254+
'Content-Disposition: form-data; name="{0}"'.format(escape_quote(name)),
255+
'',
256+
str(value),
257+
))
258+
259+
for name, value in files.items():
260+
filename = value['filename']
261+
if 'mimetype' in value:
262+
mimetype = value['mimetype']
263+
else:
264+
mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
265+
lines.extend((
266+
'--{0}'.format(boundary),
267+
'Content-Disposition: form-data; name="{0}"; filename="{1}"'.format(
268+
escape_quote(name), escape_quote(filename)),
269+
'Content-Type: {0}'.format(mimetype),
270+
'',
271+
value['content'],
272+
))
273+
274+
lines.extend((
275+
'--{0}--'.format(boundary),
276+
'',
277+
))
278+
body = '\r\n'.join(lines)
279+
280+
headers = {
281+
'Content-Type': 'multipart/form-data; boundary={0}'.format(boundary),
282+
'Content-Length': str(len(body)),
283+
}
284+
285+
return (body, headers)
286+
287+
if __name__ == '__main__':
288+
import doctest
289+
doctest.testmod()
290+
291+
157292
def process(obj):
158293
"""
159294
Process each block of the merger object.
@@ -176,9 +311,28 @@ def process(obj):
176311
if obj.get('cssmin'):
177312
cssMin(merged, obj['cssmin'])
178313

179-
314+
180315
if __name__ == '__main__':
316+
if len(sys.argv) > 2:
317+
print """Error! Use jscssmin.py [--images|--images-full]"""
318+
exit(1)
319+
320+
img, imgfull = False, False
321+
if len(sys.argv) == 2:
322+
if sys.argv[1] == '--images': img = True
323+
elif sys.argv[1] == '--images-full': img, imgfull = True, True
324+
else:
325+
print """Error! Use jscssmin.py [--images|--images-full]"""
326+
exit(1)
327+
181328
for k, j in merger['blocks'].items():
182329
print '\nProcessing block: {}'.format(k)
183330
process(j)
184-
331+
332+
if img:
333+
for root, dirs, files in os.walk(os.getcwd()):
334+
for f in files:
335+
if f.endswith('.jpg'):
336+
jpgMin(os.path.join(root, f), imgfull)
337+
elif f.endswith('.png'):
338+
pngMin(os.path.join(root, f), imgfull)

0 commit comments

Comments
 (0)