better_figures_and_images.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. """
  2. Better Figures & Images
  3. ------------------------
  4. This plugin:
  5. - Adds a style="width: ???px; height: auto;" to each image in the content
  6. - Also adds the width of the contained image to any parent div.figures.
  7. - If RESPONSIVE_IMAGES == True, also adds style="max-width: 100%;"
  8. - Corrects alt text: if alt == image filename, set alt = ''
  9. TODO: Need to add a test.py for this plugin.
  10. """
  11. from __future__ import unicode_literals
  12. from os import path, access, R_OK
  13. import os
  14. from pelican import signals
  15. from bs4 import BeautifulSoup
  16. from PIL import Image
  17. import pysvg.parser
  18. import cssutils
  19. import logging
  20. logger = logging.getLogger(__name__)
  21. def content_object_init(instance):
  22. if instance._content is not None:
  23. content = instance._content
  24. soup = BeautifulSoup(content, 'html.parser')
  25. for img in soup(['img', 'object']):
  26. logger.debug('Better Fig. PATH: %s', instance.settings['PATH'])
  27. if img.name == 'img':
  28. logger.debug('Better Fig. img.src: %s', img['src'])
  29. img_path, img_filename = path.split(img['src'])
  30. else:
  31. logger.debug('Better Fig. img.data: %s', img['data'])
  32. img_path, img_filename = path.split(img['data'])
  33. logger.debug('Better Fig. img_path: %s', img_path)
  34. logger.debug('Better Fig. img_fname: %s', img_filename)
  35. # If the image already has attributes... then we can skip it. Assuming it's already optimised
  36. if 'style' in img.attrs:
  37. sheet = cssutils.parseStyle(img['style'])
  38. if len(sheet.width) > 0 or len(sheet.height) > 0:
  39. continue
  40. # Pelican 3.5+ supports {attach} macro for auto copy, in this use case the content does not exist in output
  41. # due to the fact it has not been copied, hence we take it from the source (same as current document)
  42. src = None
  43. if img_filename.startswith('{attach}'):
  44. img_path = os.path.dirname(instance.source_path)
  45. img_filename = img_filename[8:]
  46. src = os.path.join(img_path, img_filename)
  47. elif img_path.startswith(('{filename}', '|filename|')):
  48. # Strip off {filename}, |filename| or /static
  49. img_path = img_path[10:]
  50. elif img_path.startswith('/static'):
  51. img_path = img_path[7:]
  52. elif img_path.startswith('data:image'):
  53. # Image is encoded in-line (not a file).
  54. continue
  55. else:
  56. # Check the location in the output as some plugins create them there.
  57. output_path = path.dirname(instance.save_as)
  58. image_output_location = path.join(instance.settings['OUTPUT_PATH'], output_path, img_filename)
  59. if path.isfile(image_output_location):
  60. src = image_output_location
  61. logger.info('{src} located in output, missing from content.'.format(src=img_filename))
  62. else:
  63. logger.warning('Better Fig. Error: img_path should start with either {attach}, {filename}, |filename| or /static')
  64. if src is None:
  65. # search src path list
  66. # 1. Build the source image filename from PATH
  67. # 2. Build the source image filename from STATIC_PATHS
  68. # if img_path start with '/', remove it.
  69. img_path = os.path.sep.join([el for el in img_path.split("/") if len(el) > 0])
  70. # style: {filename}/static/foo/bar.png
  71. src = os.path.join(instance.settings['PATH'], img_path, img_filename)
  72. src_candidates = [src]
  73. # style: {filename}../static/foo/bar.png
  74. src_candidates += [os.path.join(instance.settings['PATH'], static_path, img_path, img_filename) for static_path in instance.settings['STATIC_PATHS']]
  75. src_candidates = [f for f in src_candidates if path.isfile(f) and access(f, R_OK)]
  76. if not src_candidates:
  77. logger.error('Better Fig. Error: image not found: %s', src)
  78. logger.debug('Better Fig. Skip src: %s', img_path + '/' + img_filename)
  79. continue
  80. src = src_candidates[0]
  81. logger.debug('Better Fig. src: %s', src)
  82. # Open the source image and query dimensions; build style string
  83. try:
  84. if img.name == 'img':
  85. im = Image.open(src)
  86. extra_style = 'width: {}px; height: auto;'.format(im.size[0])
  87. else:
  88. svg = pysvg.parser.parse(src)
  89. extra_style = 'width: {}px; height: auto;'.format(svg.get_width())
  90. except IOError as e:
  91. logger.debug('Better Fig. Failed to open: %s', src)
  92. extra_style = 'width: 100%; height: auto;'
  93. if 'RESPONSIVE_IMAGES' in instance.settings and instance.settings['RESPONSIVE_IMAGES']:
  94. extra_style += ' max-width: 100%;'
  95. if img.get('style'):
  96. img['style'] += extra_style
  97. else:
  98. img['style'] = extra_style
  99. if img.name == 'img':
  100. if img['alt'] == img['src']:
  101. img['alt'] = ''
  102. fig = img.find_parent('div', 'figure')
  103. if fig:
  104. if fig.get('style'):
  105. fig['style'] += extra_style
  106. else:
  107. fig['style'] = extra_style
  108. instance._content = soup.decode()
  109. def register():
  110. signals.content_object_init.connect(content_object_init)