Skip to content

Commit a376a41

Browse files
committed
complete embed_images with doctest, unittest, documentation. ref pull request #2 and commit bdd8fbc4a30a
* skip http, https, and data schemes * print out message and keep image tag untouched when file is not found * BaseHandler.SUPPORT_EMBED_IMAGES is added for subclass to turn off as text handler does
1 parent ab4a26d commit a376a41

14 files changed

+253
-37
lines changed

CHANGES.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ CHANGES
55
Development
66
===========
77

8+
* add ``embed_images`` configuration option to embed image files via data URI
9+
scheme for all but text handler (pull request #2, by Adam Kemp)
10+
11+
* skip ``http``, ``https``, and ``data`` schemes
12+
* if file not found, a message is printed out, rendered HTML tag is kept
13+
untouched
14+
* related doctest and unittest tests are added, testing ``embed_images``
15+
function and with ``generate``, if with text handler, it will raise
16+
``RuntimeError`` or treat ``img`` tag as plain text, respectively
17+
* ``BaseHandler`` has class attribute ``SUPPORT_EMBED_IMAGES`` for subclass
18+
to turn off the support as text handler utilizing it
19+
820
Version 0.8.0 (2014-08-26T12:17:09Z)
921
====================================
1022

bpy/handlers/__init__.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@
5656
5757
# use smartypant to process the output of markup processor
5858
'smartypants': False,
59+
60+
# support image embedding via data URI scheme
61+
'embed_images': False,
5962
},
6063
},
6164
}
@@ -94,6 +97,40 @@
9497
9598
.. _smartypants: https://pypi.python.org/pypi/smartypants
9699
100+
.. _embed_images:
101+
102+
``embed_images``
103+
----------------
104+
105+
.. note::
106+
107+
Only :mod:`bpy.handlers.text` does not support this option.
108+
109+
When this option is enabled, it looks for the ``src`` attribute of ``img`` tag
110+
in rendered HTML, see if there is a local files, excluding ``http``, ``https``,
111+
and ``data`` schemes, if found, it reads the file and embeds with Base64
112+
encoded content.
113+
114+
For example, in reStructuredText:
115+
116+
.. code:: rst
117+
118+
.. image:: /path/to/test.png
119+
120+
Instead of
121+
122+
.. code:: html
123+
124+
<img alt="/path/to/test.png" src="/path/to/test.png" />
125+
126+
It could be replaced with, if ``/path/to/test.png`` exists:
127+
128+
.. code:: html
129+
130+
<img alt="/path/to/test.png" src="data:image/png;base64,..." />
131+
132+
If the image file can't be found, a message will be printed out, the rendered
133+
image tag will be kept untouched.
97134
98135
.. _custom-handler:
99136

bpy/handlers/asciidoc.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (C) 2013 by Yu-Jie Lin
2+
# Copyright (C) 2013, 2014 Yu-Jie Lin
33
#
44
# Permission is hereby granted, free of charge, to any person obtaining a copy
55
# of this software and associated documentation files (the "Software"), to deal
@@ -20,7 +20,17 @@
2020
# THE SOFTWARE.
2121

2222
"""
23-
No options are available at this moment.
23+
You can specify embed_images_, for example:
24+
25+
.. code:: python
26+
27+
handlers = {
28+
'AsciiDoc': {
29+
'options': {
30+
'embed_images': True,
31+
},
32+
},
33+
}
2434
"""
2535

2636
from __future__ import print_function, unicode_literals

bpy/handlers/base.py

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2013 by Yu-Jie Lin
1+
# Copyright (C) 2013, 2014 Yu-Jie Lin
22
#
33
# Permission is hereby granted, free of charge, to any person obtaining a copy
44
# of this software and associated documentation files (the "Software"), to deal
@@ -25,9 +25,9 @@
2525
import re
2626
import warnings
2727
from abc import ABCMeta, abstractmethod
28-
from hashlib import md5
29-
from os.path import basename, splitext, exists
3028
from base64 import b64encode
29+
from hashlib import md5
30+
from os.path import basename, exists, splitext
3131

3232
HAS_SMARTYPANTS = False
3333
try:
@@ -58,7 +58,15 @@ class BaseHandler():
5858
re.DOTALL | re.MULTILINE)
5959
RE_HEADER = re.compile(r'.*?([a-zA-Z0-9_-]+)\s*[=:]\s*(.*)\s*')
6060

61-
RE_IMG = re.compile(r'(<img.*?)src="([^"]*)"(.*?>)')
61+
SUPPORT_EMBED_IMAGES = True
62+
RE_IMG = re.compile(
63+
r'''
64+
(?P<prefix><img.*?)
65+
src="(?!data:image/|https?://)(?P<src>[^"]*)"
66+
(?P<suffix>.*?>)
67+
''',
68+
re.VERBOSE
69+
)
6270

6371
def __init__(self, filename, options=None):
6472

@@ -233,8 +241,8 @@ def generate(self, markup=None):
233241
Attr = smartypants.Attr
234242
html = smartypants.smartypants(html, Attr.set1 | Attr.w)
235243

236-
if self.options.get('embed_images', False):
237-
html = self.embed_images(html)
244+
if self.SUPPORT_EMBED_IMAGES and self.options.get('embed_images', False):
245+
html = self.embed_images(html)
238246

239247
return html
240248

@@ -356,21 +364,36 @@ def write(self, forced=False):
356364
self.modified = False
357365

358366
def embed_images(self, html):
359-
return self.RE_IMG.sub(self.embed_image, html)
360-
361-
def embed_image(self, match):
362-
img_name = match.group(2)
363-
if exists(img_name):
364-
img_prefix = match.group(1)
365-
src_attr = "src=\"" + self.encode_image(img_name) + "\""
366-
img_suffix = match.group(3)
367-
return img_prefix + src_attr + img_suffix
368-
else:
367+
"""Embed images on local filesystem as data URI
368+
369+
>>> class Handler(BaseHandler):
370+
... def _generate(self, source=None): return source
371+
>>> handler = Handler(None)
372+
>>> html = '<img src="http://example.com/example.png"/>'
373+
>>> print(handler.embed_images(html))
374+
<img src="http://example.com/example.png"/>
375+
>>> html = '<img src="tests/test.png"/>'
376+
>>> print(handler.embed_images(html)) #doctest: +ELLIPSIS
377+
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB...QmCC"/>
378+
"""
379+
if not self.SUPPORT_EMBED_IMAGES:
380+
raise RuntimeError('%r does not support embed_images' % type(self))
381+
382+
return self.RE_IMG.sub(self._embed_image, html)
383+
384+
@staticmethod
385+
def _embed_image(match):
386+
387+
src = match.group('src')
388+
if not exists(src):
389+
print('%s is not found.' % src)
369390
return match.group(0)
370391

371-
def encode_image(self, name):
372-
with open(name, "rb") as f:
373-
image_data = f.read()
374-
ext = splitext(name)[1].lstrip('.')
375-
encoded_image = b64encode(image_data)
376-
return "data:image/" + ext + ";base64," + encoded_image
392+
with open(src, 'rb') as f:
393+
data = b64encode(f.read()).decode('ascii')
394+
395+
return '%ssrc="%s"%s' % (
396+
match.group('prefix'),
397+
'data:image/%s;base64,%s' % (splitext(src)[1].lstrip('.'), data),
398+
match.group('suffix'),
399+
)

bpy/handlers/html.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2013 by Yu-Jie Lin
1+
# Copyright (C) 2013, 2014 Yu-Jie Lin
22
#
33
# Permission is hereby granted, free of charge, to any person obtaining a copy
44
# of this software and associated documentation files (the "Software"), to deal
@@ -22,7 +22,17 @@
2222
HTML handler simply takes the file content as its output, and assume it's valid
2323
HTML, therefore the handler doesn't edit or validate the content.
2424
25-
No options are available at this moment.
25+
You can specify embed_images_, for example:
26+
27+
.. code:: python
28+
29+
handlers = {
30+
'HTML': {
31+
'options': {
32+
'embed_images': True,
33+
},
34+
},
35+
}
2636
"""
2737

2838
from __future__ import print_function, unicode_literals

bpy/handlers/mkd.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2013 by Yu-Jie Lin
1+
# Copyright (C) 2013, 2014 Yu-Jie Lin
22
#
33
# Permission is hereby granted, free of charge, to any person obtaining a copy
44
# of this software and associated documentation files (the "Software"), to deal
@@ -19,8 +19,8 @@
1919
# THE SOFTWARE.
2020

2121
"""
22-
You can specify `configuration`__ for Python Markdown in :ref:`brc.py`, for
23-
example:
22+
You can specify `configuration`__ for Python Markdown in :ref:`brc.py` or
23+
embed_images_, for example:
2424
2525
__ http://packages.python.org/Markdown/reference.html#markdown
2626
@@ -33,6 +33,7 @@
3333
'extensions': ['extension1', 'extension2'],
3434
'tab_length': 8,
3535
},
36+
'embed_images': True,
3637
},
3738
},
3839
}

bpy/handlers/rst.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2011-2013 by Yu-Jie Lin
1+
# Copyright (C) 2011-2014 Yu-Jie Lin
22
#
33
# Permission is hereby granted, free of charge, to any person obtaining a copy
44
# of this software and associated documentation files (the "Software"), to deal
@@ -19,14 +19,15 @@
1919
# THE SOFTWARE.
2020

2121
"""
22-
You can specify settings-overrides_ for reStructuredText in :ref:`brc.py`, for
23-
example:
22+
You can specify settings-overrides_ for reStructuredText in :ref:`brc.py` or
23+
the embed_images_, for example:
2424
2525
.. code:: python
2626
2727
handlers = {
2828
'reStructuredText': {
2929
'options': {
30+
'embed_images': True,
3031
'register_directives': {
3132
'dir_name': MyDir,
3233
},

bpy/handlers/text.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2013 by Yu-Jie Lin
1+
# Copyright (C) 2013, 2014 Yu-Jie Lin
22
#
33
# Permission is hereby granted, free of charge, to any person obtaining a copy
44
# of this software and associated documentation files (the "Software"), to deal
@@ -69,6 +69,8 @@ class Handler(base.BaseHandler):
6969
PREFIX_END = ''
7070
HEADER_FMT = '%s: %s'
7171

72+
SUPPORT_EMBED_IMAGES = False
73+
7274
def generate_title(self, markup=None):
7375
"""Generate HTML from plain text
7476

tests/test.png

69 Bytes
Loading

tests/test_bpy_handlers_asciidoc.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,17 @@ def setUp(self):
5252
# =====
5353

5454
test_smartypants_EXPECT = '<p>foo &#8220;bar&#8221;</p>'
55+
56+
# =====
57+
58+
@unittest.skip('tested in BaseHandler')
59+
def test_embed_images(self):
60+
61+
pass
62+
63+
test_embed_images_generate_SOURCE = 'image:tests/test.png[tests/test.png]'
64+
test_embed_images_generate_EXPECT = (
65+
'<p><img src="%s" style="border-width: 0;" alt="tests/test.png"></p>' % (
66+
test_base.BaseHandlerTestCase.test_embed_images_data_URI
67+
)
68+
)

tests/test_bpy_handlers_base.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2013 by Yu-Jie Lin
1+
# Copyright (C) 2013, 2014 Yu-Jie Lin
22
#
33
# Permission is hereby granted, free of charge, to any person obtaining a copy
44
# of this software and associated documentation files (the "Software"), to deal
@@ -360,3 +360,60 @@ def test_update_source(self):
360360

361361
handler.update_source()
362362
self.assertEqual(handler.source, source)
363+
364+
# =====
365+
366+
test_embed_images_src = 'tests/test.png'
367+
test_embed_images_data_URI = (
368+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAI'
369+
'AAACQd1PeAAAADElEQVQI12Oorq4GAALmAXLRBAkWAAAAAElFTkSuQmCC'
370+
)
371+
372+
test_embed_images_SOURCE1 = '<img src="http://example.com/example.png"/>'
373+
test_embed_images_EXPECT1 = test_embed_images_SOURCE1
374+
375+
test_embed_images_SOURCE2 = '<img src="tests/test.png"/>'
376+
test_embed_images_EXPECT2 = '<img src="%s"/>' % test_embed_images_data_URI
377+
378+
test_embed_images_SOURCE3 = '<img alt="foo" src="tests/test.png"/>'
379+
test_embed_images_EXPECT3 = '<img alt="foo" src="%s"/>' % (
380+
test_embed_images_data_URI)
381+
382+
test_embed_images_SOURCE4 = '<img src="tests/test.png" title="bar"/>'
383+
test_embed_images_EXPECT4 = '<img src="%s" title="bar"/>' % (
384+
test_embed_images_data_URI)
385+
386+
test_embed_images_SOURCE5 = '<img src="%s"/>' % test_embed_images_data_URI
387+
test_embed_images_EXPECT5 = test_embed_images_SOURCE5
388+
389+
def test_embed_images(self):
390+
391+
handler = self.handler
392+
393+
result = handler.embed_images(self.test_embed_images_SOURCE1)
394+
self.assertEqual(result, self.test_embed_images_EXPECT1)
395+
396+
result = handler.embed_images(self.test_embed_images_SOURCE2)
397+
self.assertEqual(result, self.test_embed_images_EXPECT2)
398+
399+
result = handler.embed_images(self.test_embed_images_SOURCE3)
400+
self.assertEqual(result, self.test_embed_images_EXPECT3)
401+
402+
result = handler.embed_images(self.test_embed_images_SOURCE4)
403+
self.assertEqual(result, self.test_embed_images_EXPECT4)
404+
405+
result = handler.embed_images(self.test_embed_images_SOURCE5)
406+
self.assertEqual(result, self.test_embed_images_EXPECT5)
407+
408+
test_embed_images_generate_SOURCE = '<img src="tests/test.png"/>'
409+
test_embed_images_generate_EXPECT = '<img src="%s"/>' % (
410+
test_embed_images_data_URI)
411+
412+
def test_embed_images_generate(self):
413+
414+
handler = self.handler
415+
handler.options['embed_images'] = True
416+
417+
handler.markup = self.test_embed_images_generate_SOURCE
418+
html = handler.generate()
419+
self.assertEqual(html, self.test_embed_images_generate_EXPECT)

0 commit comments

Comments
 (0)