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 picture import tasks
16 from wolnelektury.utils import cached_render, clear_cached_renders
17 from io import BytesIO
25 from newtagging import managers
29 picture_storage = FileSystemStorage(location=path.join(
30 settings.MEDIA_ROOT, 'pictures'),
31 base_url=settings.MEDIA_URL + "pictures/")
34 class PictureArea(models.Model):
35 picture = models.ForeignKey('picture.Picture', models.CASCADE, related_name='areas')
36 area = models.TextField('obszar', default='{}', editable=False)
37 kind = models.CharField(
38 'typ', max_length=10, blank=False, null=False, db_index=True,
39 choices=(('thing', 'przedmiot'), ('theme', 'motyw'))
42 objects = models.Manager()
43 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
44 tags = managers.TagDescriptor(catalogue.models.Tag)
45 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
47 short_html_url_name = 'picture_area_short'
50 def rectangle(cls, picture, kind, coords):
54 pa.area = json.dumps(coords)
57 def get_area_json(self):
58 return json.loads(self.area)
61 class Picture(models.Model):
66 title = models.CharField('tytuł', max_length=32767)
67 slug = models.SlugField('slug', max_length=120, db_index=True, unique=True)
68 sort_key = models.CharField('klucz sortowania', max_length=120, db_index=True, editable=False)
69 sort_key_author = models.CharField(
70 'klucz sortowania wg autora', max_length=120, db_index=True, editable=False, default='')
71 created_at = models.DateTimeField('data utworzenia', auto_now_add=True, db_index=True)
72 changed_at = models.DateTimeField('data zmiany', auto_now=True, db_index=True)
73 xml_file = models.FileField('plik xml', upload_to="xml", storage=picture_storage)
74 image_file = ImageField('plik obrazu', upload_to="images", storage=picture_storage)
75 html_file = models.FileField('plik html', upload_to="html", storage=picture_storage)
76 areas_json = models.TextField('obszary w JSON', default='{}', editable=False)
77 extra_info = models.TextField('dodatkowa informacja', default='{}')
78 culturepl_link = models.CharField(blank=True, max_length=240)
79 wiki_link = models.CharField(blank=True, max_length=240)
81 width = models.IntegerField(null=True)
82 height = models.IntegerField(null=True)
84 objects = models.Manager()
85 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
86 tags = managers.TagDescriptor(catalogue.models.Tag)
87 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
89 short_html_url_name = 'picture_short'
93 class AlreadyExists(Exception):
97 ordering = ('sort_key_author', 'sort_key')
99 verbose_name = 'obraz'
100 verbose_name_plural = 'obrazy'
102 def get_extra_info_json(self):
103 return json.loads(self.extra_info or '{}')
105 def save(self, force_insert=False, force_update=False, **kwargs):
106 from sortify import sortify
108 self.sort_key = sortify(self.title)[:120]
111 author = self.authors().first().sort_key
112 except AttributeError:
114 self.sort_key_author = author
116 ret = super(Picture, self).save(force_insert, force_update)
124 return self.tags.filter(category='author')
127 return self.tags.filter(category='epoch')
130 return self.tags.filter(category='genre')
133 return self.tags.filter(category='kind')
135 def tag_unicode(self, category):
136 relations = prefetched_relations(self, category)
138 return ', '.join(rel.tag.name for rel in relations)
140 return ', '.join(self.tags.filter(category=category).values_list('name', flat=True))
142 def author_unicode(self):
143 return self.tag_unicode('author')
145 def tags_by_category(self):
146 return split_tags(self.tags)
148 def get_absolute_url(self):
149 return reverse('picture_detail', args=[self.slug])
151 def get_initial(self):
153 return re.search(r'\w', self.title, re.U).group(0)
154 except AttributeError:
159 return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
163 def get_previous(self):
165 return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
170 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False):
172 Import xml and it's accompanying image file.
173 If image file is missing, it will be fetched by librarian.picture.ImageStore
174 which looks for an image file in the same directory the xml is, with extension matching
177 from sortify import sortify
178 from django.core.files import File
179 from librarian.picture import WLPicture, ImageStore
180 close_xml_file = False
181 close_image_file = False
183 if image_file is not None and not isinstance(image_file, File):
184 image_file = File(open(image_file, 'rb'))
185 close_image_file = True
187 if not isinstance(xml_file, File):
188 xml_file = File(open(xml_file))
189 close_xml_file = True
191 with transaction.atomic():
192 # use librarian to parse meta-data
193 if image_store is None:
194 image_store = ImageStore(picture_storage.path('images'))
195 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
197 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
198 if not created and not overwrite:
199 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
201 picture.areas.all().delete()
202 picture.title = str(picture_xml.picture_info.title)
203 picture.extra_info = json.dumps(picture_xml.picture_info.to_dict())
205 picture_tags = set([t for (t, rel) in catalogue.models.Tag.tags_from_info(picture_xml.picture_info)])
206 for tag in picture_tags:
207 if not tag.for_pictures:
208 tag.for_pictures = True
211 area_data = {'themes': {}, 'things': {}}
213 # Treat all names in picture XML as in default language.
214 lang = settings.LANGUAGE_CODE
216 for part in picture_xml.partiter():
217 if picture_xml.frame:
218 c = picture_xml.frame[0]
219 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
220 if part.get('object', None) is not None:
222 for objname in part['object'].split(','):
223 objname = objname.strip()
224 assert objname, 'Empty object name'
225 # str.capitalize() is wrong, because it also lowers letters
226 objname = objname[0].upper() + objname[1:]
227 tag, created = catalogue.models.Tag.objects.get_or_create(
228 slug=slugify(objname), category='thing')
231 setattr(tag, 'name_%s' % lang, tag.name)
232 tag.sort_key = sortify(tag.name)
233 tag.for_pictures = True
235 area_data['things'][tag.slug] = {
237 'coords': part['coords'],
241 if not tag.for_pictures:
242 tag.for_pictures = True
244 area = PictureArea.rectangle(picture, 'thing', part['coords'])
246 # WTF thing area does not inherit tags from picture and theme area does, is it intentional?
250 for motifs in part['themes']:
251 for motif in motifs.split(','):
252 tag, created = catalogue.models.Tag.objects.get_or_create(
253 slug=slugify(motif), category='theme')
256 tag.sort_key = sortify(tag.name)
257 tag.for_pictures = True
259 # motif_tags.add(tag)
261 if not tag.for_pictures:
262 tag.for_pictures = True
264 area_data['themes'][tag.slug] = {
266 'coords': part['coords']
269 logging.debug("coords for theme: %s" % part['coords'])
270 area = PictureArea.rectangle(picture, 'theme', part['coords'])
272 area.tags = _tags.union(picture_tags)
274 picture.tags = picture_tags
275 picture.areas_json = json.dumps(area_data)
277 if image_file is not None:
280 img = picture_xml.image_file()
282 modified = cls.crop_to_frame(picture_xml, img)
283 modified = cls.add_source_note(picture_xml, modified)
285 picture.width, picture.height = modified.size
287 modified_file = BytesIO()
288 modified.save(modified_file, format='JPEG', quality=95)
289 # FIXME: hardcoded extension - detect from DC format or orginal filename
290 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
292 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
294 tasks.generate_picture_html(picture.id)
304 def crop_to_frame(cls, wlpic, image_file):
305 img = Image.open(image_file)
306 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
308 img = img.crop(itertools.chain(*wlpic.frame))
312 def add_source_note(wlpic, img):
313 from PIL import ImageDraw, ImageFont
314 from librarian import get_resource
316 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
317 annotated.paste(img, (0, 0))
318 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
319 ImageDraw.Draw(annotation).text(
321 wlpic.picture_info.source_name,
323 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
325 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
330 if not hasattr(self, '_info'):
331 from librarian import dcparser
332 from librarian import picture
333 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
337 def pretty_title(self, html_links=False):
338 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
339 names.append((self.title, self.get_absolute_url()))
342 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
344 names = [tag[0] for tag in names]
345 return ', '.join(names)
347 @cached_render('picture/picture_mini_box.html')
353 def related_themes(self):
354 return catalogue.models.Tag.objects.usage_for_queryset(
355 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
357 def clear_cache(self):
358 clear_cached_renders(self.mini_box)