A fork of hyde, the static site generation. Some patches will be pushed upstream.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

279 lines
11 KiB

  1. # -*- coding: utf-8 -*-
  2. """
  3. Contains classes to handle images related things
  4. # Requires PIL
  5. """
  6. from hyde.plugin import Plugin
  7. from hyde.fs import File, Folder
  8. import re
  9. import Image
  10. import glob
  11. import os
  12. class ImageSizerPlugin(Plugin):
  13. """
  14. Each HTML page is modified to add width and height for images if
  15. they are not already specified.
  16. """
  17. def __init__(self, site):
  18. super(ImageSizerPlugin, self).__init__(site)
  19. self.cache = {}
  20. def _handle_img(self, resource, src, width, height):
  21. """Determine what should be added to an img tag"""
  22. if height is not None and width is not None:
  23. return "" # Nothing
  24. if src is None:
  25. self.logger.warn("[%s] has an img tag without src attribute" % resource)
  26. return "" # Nothing
  27. if src not in self.cache:
  28. if src.startswith(self.site.config.media_url):
  29. path = src[len(self.site.config.media_url):].lstrip("/")
  30. path = self.site.config.media_root_path.child(path)
  31. image = self.site.content.resource_from_relative_deploy_path(path)
  32. elif re.match(r'([a-z]+://|//).*', src):
  33. # Not a local link
  34. return "" # Nothing
  35. elif src.startswith("/"):
  36. # Absolute resource
  37. path = src.lstrip("/")
  38. image = self.site.content.resource_from_relative_deploy_path(path)
  39. else:
  40. # Relative resource
  41. path = resource.node.source_folder.child(src)
  42. image = self.site.content.resource_from_path(path)
  43. if image is None:
  44. self.logger.warn(
  45. "[%s] has an unknown image" % resource)
  46. return "" # Nothing
  47. if image.source_file.kind not in ['png', 'jpg', 'jpeg', 'gif']:
  48. self.logger.warn(
  49. "[%s] has an img tag not linking to an image" % resource)
  50. return "" # Nothing
  51. # Now, get the size of the image
  52. try:
  53. self.cache[src] = Image.open(image.path).size
  54. except IOError:
  55. self.logger.warn(
  56. "Unable to process image [%s]" % image)
  57. self.cache[src] = (None, None)
  58. return "" # Nothing
  59. self.logger.debug("Image [%s] is %s" % (src,
  60. self.cache[src]))
  61. new_width, new_height = self.cache[src]
  62. if new_width is None or new_height is None:
  63. return "" # Nothing
  64. if width is not None:
  65. return 'height="%d" ' % (int(width)*new_height/new_width)
  66. elif height is not None:
  67. return 'width="%d" ' % (int(height)*new_width/new_height)
  68. return 'height="%d" width="%d" ' % (new_height, new_width)
  69. def text_resource_complete(self, resource, text):
  70. """
  71. When the resource is generated, search for img tag and specify
  72. their sizes.
  73. Some img tags may be missed, this is not a perfect parser.
  74. """
  75. try:
  76. mode = self.site.config.mode
  77. except AttributeError:
  78. mode = "production"
  79. if not resource.source_file.kind == 'html':
  80. return
  81. if mode.startswith('dev'):
  82. self.logger.debug("Skipping sizer in development mode.")
  83. return
  84. pos = 0 # Position in text
  85. img = None # Position of current img tag
  86. state = "find-img"
  87. while pos < len(text):
  88. if state == "find-img":
  89. img = text.find("<img", pos)
  90. if img == -1:
  91. break # No more img tag
  92. pos = img + len("<img")
  93. if not text[pos].isspace():
  94. continue # Not an img tag
  95. pos = pos + 1
  96. tags = {"src": "",
  97. "width": "",
  98. "height": ""}
  99. state = "find-attr"
  100. continue
  101. if state == "find-attr":
  102. if text[pos] == ">":
  103. # We get our img tag
  104. insert = self._handle_img(resource,
  105. tags["src"] or None,
  106. tags["width"] or None,
  107. tags["height"] or None)
  108. img = img + len("<img ")
  109. text = "".join([text[:img], insert, text[img:]])
  110. state = "find-img"
  111. pos = pos + 1
  112. continue
  113. attr = None
  114. for tag in tags:
  115. if text[pos:(pos+len(tag)+1)] == ("%s=" % tag):
  116. attr = tag
  117. pos = pos + len(tag) + 1
  118. break
  119. if not attr:
  120. pos = pos + 1
  121. continue
  122. if text[pos] in ["'", '"']:
  123. pos = pos + 1
  124. state = "get-value"
  125. continue
  126. if state == "get-value":
  127. if text[pos] == ">":
  128. state = "find-attr"
  129. continue
  130. if text[pos] in ["'", '"'] or text[pos].isspace():
  131. # We got our value
  132. pos = pos + 1
  133. state = "find-attr"
  134. continue
  135. tags[attr] = tags[attr] + text[pos]
  136. pos = pos + 1
  137. continue
  138. return text
  139. class ImageThumbnailsPlugin(Plugin):
  140. """
  141. Provide a function to get thumbnail for any image resource.
  142. Example of usage:
  143. Setting optional defaults in site.yaml:
  144. thumbnails:
  145. width: 100
  146. height: 120
  147. prefix: thumbnail_
  148. Setting thumbnails options in nodemeta.yaml:
  149. thumbnails:
  150. - width: 50
  151. prefix: thumbs1_
  152. include:
  153. - '*.png'
  154. - '*.jpg'
  155. - height: 100
  156. prefix: thumbs2_
  157. include:
  158. - '*.png'
  159. - '*.jpg'
  160. which means - make from every picture two thumbnails with different prefixes
  161. and sizes
  162. If both width and height defined, image would be cropped, you can define
  163. crop_type as one of these values: "topleft", "center" and "bottomright".
  164. "topleft" is default.
  165. Currently, only supports PNG and JPG.
  166. """
  167. def __init__(self, site):
  168. super(ImageThumbnailsPlugin, self).__init__(site)
  169. def thumb(self, resource, width, height, prefix, crop_type):
  170. """
  171. Generate a thumbnail for the given image
  172. """
  173. name = os.path.basename(resource.get_relative_deploy_path())
  174. # don't make thumbnails for thumbnails
  175. if name.startswith(prefix):
  176. return
  177. # Prepare path, make all thumnails in single place(content/.thumbnails)
  178. # for simple maintenance but keep original deploy path to preserve
  179. # naming logic in generated site
  180. path = os.path.join(".thumbnails",
  181. os.path.dirname(resource.get_relative_deploy_path()),
  182. "%s%s" % (prefix, name))
  183. target = File(Folder(resource.site.config.content_root_path).child(path))
  184. res = self.site.content.add_resource(target)
  185. res.set_relative_deploy_path(res.get_relative_deploy_path().replace('.thumbnails/', '', 1))
  186. target.parent.make()
  187. if os.path.exists(target.path) and os.path.getmtime(resource.path) <= os.path.getmtime(target.path):
  188. return
  189. self.logger.debug("Making thumbnail for [%s]" % resource)
  190. im = Image.open(resource.path)
  191. if im.mode != 'RGBA':
  192. im = im.convert('RGBA')
  193. resize_width = width
  194. resize_height = height
  195. if resize_width is None:
  196. resize_width = im.size[0]*height/im.size[1] + 1
  197. elif resize_height is None:
  198. resize_height = im.size[1]*width/im.size[0] + 1
  199. elif im.size[0]*height >= im.size[1]*width:
  200. resize_width = im.size[0]*height/im.size[1]
  201. else:
  202. resize_height = im.size[1]*width/im.size[0]
  203. im = im.resize((resize_width, resize_height), Image.ANTIALIAS)
  204. if width is not None and height is not None:
  205. shiftx = shifty = 0
  206. if crop_type == "center":
  207. shiftx = (im.size[0] - width)/2
  208. shifty = (im.size[1] - height)/2
  209. elif crop_type == "bottomright":
  210. shiftx = (im.size[0] - width)
  211. shifty = (im.size[1] - height)
  212. im = im.crop((shiftx, shifty, width + shiftx, height + shifty))
  213. im.load()
  214. if resource.name.endswith(".jpg"):
  215. im.save(target.path, "JPEG", optimize=True, quality=75)
  216. else:
  217. im.save(target.path, "PNG", optimize=True)
  218. def begin_site(self):
  219. """
  220. Find any image resource that should be thumbnailed and call thumb on it.
  221. """
  222. # Grab default values from config
  223. config = self.site.config
  224. defaults = { "width": None,
  225. "height": None,
  226. "crop_type": "topleft",
  227. "prefix": 'thumb_'}
  228. if hasattr(config, 'thumbnails'):
  229. defaults.update(config.thumbnails)
  230. for node in self.site.content.walk():
  231. if hasattr(node, 'meta') and hasattr(node.meta, 'thumbnails'):
  232. for th in node.meta.thumbnails:
  233. if not hasattr(th, 'include'):
  234. self.logger.error("Include is not set for node [%s]" % node)
  235. continue
  236. include = th.include
  237. prefix = th.prefix if hasattr(th, 'prefix') else defaults['prefix']
  238. height = th.height if hasattr(th, 'height') else defaults['height']
  239. width = th.width if hasattr(th, 'width') else defaults['width']
  240. crop_type = th.crop_type if hasattr(th, 'crop_type') else defaults['crop_type']
  241. if crop_type not in ["topleft", "center", "bottomright"]:
  242. self.logger.error("Unknown crop_type defined for node [%s]" % node)
  243. continue
  244. if width is None and height is None:
  245. self.logger.error("Both width and height are not set for node [%s]" % node)
  246. continue
  247. thumbs_list = []
  248. for inc in include:
  249. for path in glob.glob(node.path + os.sep + inc):
  250. thumbs_list.append(path)
  251. for resource in node.resources:
  252. if resource.source_file.kind in ["jpg", "png"] and resource.path in thumbs_list:
  253. self.thumb(resource, width, height, prefix, crop_type)