1 # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
4 from django.db import models, transaction
5 import catalogue.models
6 from sorl.thumbnail import ImageField
7 from django.conf import settings
8 from django.contrib.contenttypes.fields import GenericRelation
9 from django.core.files.storage import FileSystemStorage
10 from django.urls import reverse
11 from slugify import slugify
13 from catalogue.models.tag import prefetched_relations
14 from catalogue.utils import split_tags
15 from wolnelektury.utils import cached_render, clear_cached_renders
16 from io import BytesIO
24 from newtagging import managers
28 picture_storage = FileSystemStorage(location=path.join(
29 settings.MEDIA_ROOT, 'pictures'),
30 base_url=settings.MEDIA_URL + "pictures/")
33 class PictureArea(models.Model):
34 picture = models.ForeignKey('picture.Picture', models.CASCADE, related_name='areas')
35 area = models.TextField('obszar', default='{}', editable=False)
36 kind = models.CharField(
37 'typ', max_length=10, blank=False, null=False, db_index=True,
38 choices=(('thing', 'przedmiot'), ('theme', 'motyw'))
41 objects = models.Manager()
42 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
43 tags = managers.TagDescriptor(catalogue.models.Tag)
44 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
46 short_html_url_name = 'picture_area_short'
49 def rectangle(cls, picture, kind, coords):
53 pa.area = json.dumps(coords)
56 def get_area_json(self):
57 return json.loads(self.area)
60 class Picture(models.Model):
65 title = models.CharField('tytuł', max_length=32767)
66 slug = models.SlugField('slug', max_length=120, db_index=True, unique=True)
67 sort_key = models.CharField('klucz sortowania', max_length=120, db_index=True, editable=False)
68 sort_key_author = models.CharField(
69 'klucz sortowania wg autora', max_length=120, db_index=True, editable=False, default='')
70 created_at = models.DateTimeField('data utworzenia', auto_now_add=True, db_index=True)
71 changed_at = models.DateTimeField('data zmiany', auto_now=True, db_index=True)
72 xml_file = models.FileField('plik xml', upload_to="xml", storage=picture_storage)
73 image_file = ImageField('plik obrazu', upload_to="images", storage=picture_storage)
74 html_file = models.FileField('plik html', upload_to="html", storage=picture_storage)
75 areas_json = models.TextField('obszary w JSON', default='{}', editable=False)
76 extra_info = models.TextField('dodatkowa informacja', default='{}')
77 culturepl_link = models.CharField(blank=True, max_length=240)
78 wiki_link = models.CharField(blank=True, max_length=240)
80 width = models.IntegerField(null=True)
81 height = models.IntegerField(null=True)
83 objects = models.Manager()
84 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
85 tags = managers.TagDescriptor(catalogue.models.Tag)
86 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
88 short_html_url_name = 'picture_short'
92 class AlreadyExists(Exception):
96 ordering = ('sort_key_author', 'sort_key')
98 verbose_name = 'obraz'
99 verbose_name_plural = 'obrazy'
101 def get_extra_info_json(self):
102 return json.loads(self.extra_info or '{}')
104 def save(self, force_insert=False, force_update=False, **kwargs):
105 from sortify import sortify
107 self.sort_key = sortify(self.title)[:120]
110 author = self.authors().first().sort_key
111 except AttributeError:
113 self.sort_key_author = author
115 ret = super(Picture, self).save(force_insert, force_update)
123 return self.tags.filter(category='author')
126 return self.tags.filter(category='epoch')
129 return self.tags.filter(category='genre')
132 return self.tags.filter(category='kind')
134 def tag_unicode(self, category):
135 relations = prefetched_relations(self, category)
137 return ', '.join(rel.tag.name for rel in relations)
139 return ', '.join(self.tags.filter(category=category).values_list('name', flat=True))
141 def author_unicode(self):
142 return self.tag_unicode('author')
144 def tags_by_category(self):
145 return split_tags(self.tags)
147 def get_absolute_url(self):
148 return reverse('picture_detail', args=[self.slug])
150 def get_initial(self):
152 return re.search(r'\w', self.title, re.U).group(0)
153 except AttributeError:
158 return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
162 def get_previous(self):
164 return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
169 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
171 Import xml and it's accompanying image file.
172 If image file is missing, it will be fetched by librarian.picture.ImageStore
173 which looks for an image file in the same directory the xml is, with extension matching
176 from sortify import sortify
177 from django.core.files import File
178 from librarian.picture import WLPicture, ImageStore
179 close_xml_file = False
180 close_image_file = False
182 if image_file is not None and not isinstance(image_file, File):
183 image_file = File(open(image_file, 'rb'))
184 close_image_file = True
186 if not isinstance(xml_file, File):
187 xml_file = File(open(xml_file))
188 close_xml_file = True
190 with transaction.atomic():
191 # use librarian to parse meta-data
192 if image_store is None:
193 image_store = ImageStore(picture_storage.path('images'))
194 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
196 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
197 if not created and not overwrite:
198 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
200 picture.areas.all().delete()
201 picture.title = str(picture_xml.picture_info.title)
202 picture.extra_info = json.dumps(picture_xml.picture_info.to_dict())
204 picture_tags = set([t for (t, rel) in catalogue.models.Tag.tags_from_info(picture_xml.picture_info)])
205 for tag in picture_tags:
206 if not tag.for_pictures:
207 tag.for_pictures = True
210 area_data = {'themes': {}, 'things': {}}
212 # Treat all names in picture XML as in default language.
213 lang = settings.LANGUAGE_CODE
215 for part in picture_xml.partiter():
216 if picture_xml.frame:
217 c = picture_xml.frame[0]
218 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
219 if part.get('object', None) is not None:
221 for objname in part['object'].split(','):
222 objname = objname.strip()
223 assert objname, 'Empty object name'
224 # str.capitalize() is wrong, because it also lowers letters
225 objname = objname[0].upper() + objname[1:]
226 tag, created = catalogue.models.Tag.objects.get_or_create(
227 slug=slugify(objname), category='thing')
230 setattr(tag, 'name_%s' % lang, tag.name)
231 tag.sort_key = sortify(tag.name)
232 tag.for_pictures = True
234 area_data['things'][tag.slug] = {
236 'coords': part['coords'],
240 if not tag.for_pictures:
241 tag.for_pictures = True
243 area = PictureArea.rectangle(picture, 'thing', part['coords'])
245 # WTF thing area does not inherit tags from picture and theme area does, is it intentional?
249 for motifs in part['themes']:
250 for motif in motifs.split(','):
251 tag, created = catalogue.models.Tag.objects.get_or_create(
252 slug=slugify(motif), category='theme')
255 tag.sort_key = sortify(tag.name)
256 tag.for_pictures = True
258 # motif_tags.add(tag)
260 if not tag.for_pictures:
261 tag.for_pictures = True
263 area_data['themes'][tag.slug] = {
265 'coords': part['coords']
268 logging.debug("coords for theme: %s" % part['coords'])
269 area = PictureArea.rectangle(picture, 'theme', part['coords'])
271 area.tags = _tags.union(picture_tags)
273 picture.tags = picture_tags
274 picture.areas_json = json.dumps(area_data)
276 if image_file is not None:
279 img = picture_xml.image_file()
281 modified = cls.crop_to_frame(picture_xml, img)
282 modified = cls.add_source_note(picture_xml, modified)
284 picture.width, picture.height = modified.size
286 modified_file = BytesIO()
287 modified.save(modified_file, format='JPEG', quality=95)
288 # FIXME: hardcoded extension - detect from DC format or orginal filename
289 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
291 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
302 def crop_to_frame(cls, wlpic, image_file):
303 img = Image.open(image_file)
304 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
306 img = img.crop(itertools.chain(*wlpic.frame))
310 def add_source_note(wlpic, img):
311 from PIL import ImageDraw, ImageFont
312 from librarian import get_resource
314 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
315 annotated.paste(img, (0, 0))
316 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
317 ImageDraw.Draw(annotation).text(
319 wlpic.picture_info.source_name,
321 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
323 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
328 if not hasattr(self, '_info'):
329 from librarian import dcparser
330 from librarian import picture
331 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
335 def pretty_title(self, html_links=False):
336 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
337 names.append((self.title, self.get_absolute_url()))
340 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
342 names = [tag[0] for tag in names]
343 return ', '.join(names)
345 @cached_render('picture/picture_mini_box.html')
351 def related_themes(self):
352 return catalogue.models.Tag.objects.usage_for_queryset(
353 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
355 def clear_cache(self):
356 clear_cached_renders(self.mini_box)