Images: image@src is a URL, and image sizes are limited.
[librarian.git] / src / librarian / picture.py
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from operator import and_
5
6 from .dcparser import Field, WorkInfo, DCNS
7 from librarian import (RDFNS, ValidationError, NoDublinCore, ParseError, WLURI)
8 from xml.parsers.expat import ExpatError
9 from os import path
10 from lxml import etree
11 from lxml.etree import (XMLSyntaxError, XSLTApplyError, Element)
12 import re
13 import six
14
15
16 class WLPictureURI(WLURI):
17     _re_wl_uri = re.compile(
18         'http://wolnelektury.pl/katalog/obraz/(?P<slug>[-a-z0-9]+)/?$'
19     )
20
21     @classmethod
22     def from_slug(cls, slug):
23         uri = 'http://wolnelektury.pl/katalog/obraz/%s/' % slug
24         return cls(uri)
25
26
27 def as_wlpictureuri_strict(text):
28     return WLPictureURI.strict(text)
29
30
31 class PictureInfo(WorkInfo):
32     """
33     Dublin core metadata for a picture
34     """
35     FIELDS = (
36         Field(DCNS('language'), 'language', required=False),
37         Field(DCNS('subject.period'), 'epochs', salias='epoch', multiple=True),
38         Field(DCNS('subject.type'), 'kinds', salias='kind', multiple=True),
39         Field(DCNS('subject.genre'), 'genres', salias='genre', multiple=True,
40               required=False),
41         Field(DCNS('subject.style'), 'styles', salias='style', multiple=True,
42               required=False),
43
44         Field(DCNS('format.dimensions'), 'dimensions', required=False),
45         Field(DCNS('format.checksum.sha1'), 'sha1', required=True),
46         Field(DCNS('description.medium'), 'medium', required=False),
47         Field(DCNS('description.dimensions'), 'original_dimensions',
48               required=False),
49         Field(DCNS('format'), 'mime_type', required=False),
50         Field(DCNS('identifier.url'), 'url', WLPictureURI,
51               strict=as_wlpictureuri_strict)
52     )
53
54
55 class ImageStore(object):
56     EXT = ['gif', 'jpeg', 'png', 'swf', 'psd', 'bmp'
57            'tiff', 'tiff', 'jpc', 'jp2', 'jpf', 'jb2', 'swc',
58            'aiff', 'wbmp', 'xbm']
59     MIME = ['image/gif', 'image/jpeg', 'image/png',
60             'application/x-shockwave-flash', 'image/psd', 'image/bmp',
61             'image/tiff', 'image/tiff', 'application/octet-stream',
62             'image/jp2', 'application/octet-stream',
63             'application/octet-stream', 'application/x-shockwave-flash',
64             'image/iff', 'image/vnd.wap.wbmp', 'image/xbm']
65
66     def __init__(self, dir_):
67         super(ImageStore, self).__init__()
68         self.dir = dir_
69
70     def path(self, slug, mime_type):
71         """
72         Finds file by slug and mime type in our iamge store.
73         Returns a file objects (perhaps should return a filename?)
74         """
75         try:
76             i = self.MIME.index(mime_type)
77         except ValueError:
78             err = ValueError(
79                 "Picture %s has unknown mime type: %s"
80                 % (slug, mime_type)
81             )
82             err.slug = slug
83             err.mime_type = mime_type
84             raise err
85         ext = self.EXT[i]
86         # add some common extensions tiff->tif, jpeg->jpg
87         return path.join(self.dir, slug + '.' + ext)
88
89
90 class WLPicture(object):
91     def __init__(self, edoc, parse_dublincore=True, image_store=None):
92         self.edoc = edoc
93         self.image_store = image_store
94
95         root_elem = edoc.getroot()
96
97         dc_path = './/' + RDFNS('RDF')
98
99         if root_elem.tag != 'picture':
100             raise ValidationError(
101                 "Invalid root element. Found '%s', should be 'picture'"
102                 % root_elem.tag
103             )
104
105         if parse_dublincore:
106             self.rdf_elem = root_elem.find(dc_path)
107
108             if self.rdf_elem is None:
109                 raise NoDublinCore(
110                     "Document has no DublinCore - which is required."
111                 )
112
113             self.picture_info = PictureInfo.from_element(self.rdf_elem)
114         else:
115             self.picture_info = None
116         self.frame = None
117
118     @classmethod
119     def from_bytes(cls, xml, *args, **kwargs):
120         return cls.from_file(six.BytesIO(xml), *args, **kwargs)
121
122     @classmethod
123     def from_file(cls, xmlfile, parse_dublincore=True, image_store=None):
124
125         # first, prepare for parsing
126         if isinstance(xmlfile, six.text_type):
127             file = open(xmlfile, 'rb')
128             try:
129                 data = file.read()
130             finally:
131                 file.close()
132         else:
133             data = xmlfile.read()
134
135         if not isinstance(data, six.text_type):
136             data = data.decode('utf-8')
137
138         data = data.replace(u'\ufeff', '')
139
140         # assume images are in the same directory
141         if image_store is None and getattr(xmlfile, 'name', None):
142             image_store = ImageStore(path.dirname(xmlfile.name))
143
144         try:
145             parser = etree.XMLParser(remove_blank_text=False)
146             tree = etree.parse(six.BytesIO(data.encode('utf-8')), parser)
147
148             me = cls(tree, parse_dublincore=parse_dublincore,
149                      image_store=image_store)
150             me.load_frame_info()
151             return me
152         except (ExpatError, XMLSyntaxError, XSLTApplyError) as e:
153             raise ParseError(e)
154
155     @property
156     def mime_type(self):
157         if self.picture_info is None:
158             raise ValueError(
159                 "DC is not loaded, hence we don't know the image type."
160             )
161         return self.picture_info.mime_type
162
163     @property
164     def slug(self):
165         return self.picture_info.url.slug
166
167     @property
168     def image_path(self):
169         if self.image_store is None:
170             raise ValueError("No image store associated with whis WLPicture.")
171
172         return self.image_store.path(self.slug, self.mime_type)
173
174     def image_file(self, *args, **kwargs):
175         return open(self.image_path, 'rb', *args, **kwargs)
176
177     def get_sem_coords(self, sem):
178         area = sem.find("div[@type='rect']")
179         if area is None:
180             area = sem.find("div[@type='whole']")
181             return [[0, 0], [-1, -1]]
182
183         def has_all_props(node, props):
184             return six.moves.reduce(
185                 and_, map(lambda prop: prop in node.attrib, props)
186             )
187
188         if not has_all_props(area, ['x1', 'x2', 'y1', 'y2']):
189             return None
190
191         def n(prop): return int(area.get(prop))
192         return [[n('x1'), n('y1')], [n('x2'), n('y2')]]
193
194     def partiter(self):
195         """
196         Iterates the parts of this picture and returns them
197         and their metadata.
198         """
199         # omg no support for //sem[(@type='theme') or (@type='object')] ?
200         for part in list(self.edoc.iterfind("//sem[@type='theme']")) +\
201                 list(self.edoc.iterfind("//sem[@type='object']")):
202             pd = {'type': part.get('type')}
203
204             coords = self.get_sem_coords(part)
205             if coords is None:
206                 continue
207             pd['coords'] = coords
208
209             def want_unicode(x):
210                 if not isinstance(x, six.text_type):
211                     return x.decode('utf-8')
212                 else:
213                     return x
214             pd['object'] = (
215                 part.attrib['type'] == 'object'
216                 and want_unicode(part.attrib.get('object', u''))
217                 or None
218             )
219             pd['themes'] = (
220                 part.attrib['type'] == 'theme'
221                 and [part.attrib.get('theme', u'')]
222                 or []
223             )
224             yield pd
225
226     def load_frame_info(self):
227         k = self.edoc.find("//sem[@object='kadr']")
228
229         if k is not None:
230             clip = self.get_sem_coords(k)
231             self.frame = clip
232             frm = Element("sem", {"type": "frame"})
233             frm.append(k.iter("div").next())
234             self.edoc.getroot().append(frm)
235             k.getparent().remove(k)
236         else:
237             frm = self.edoc.find("//sem[@type='frame']")
238             if frm:
239                 self.frame = self.get_sem_coords(frm)
240             else:
241                 self.frame = None
242         return self