diff --git a/app/apps/api/serializers.py b/app/apps/api/serializers.py index 6b9cc7010d5b0d27659587e53153b2386056dfda..4bc50b7aaa7a7bd5e5a3709348e17819073583c2 100644 --- a/app/apps/api/serializers.py +++ b/app/apps/api/serializers.py @@ -61,6 +61,7 @@ class DocumentSerializer(serializers.ModelSerializer): class PartSerializer(serializers.ModelSerializer): image = ImageField(thumbnails=['card', 'large']) + filename = serializers.CharField(read_only=True) bw_image = ImageField(thumbnails=['large'], required=False) workflow = serializers.JSONField(read_only=True) transcription_progress = serializers.IntegerField(read_only=True) @@ -70,6 +71,7 @@ class PartSerializer(serializers.ModelSerializer): fields = ( 'pk', 'name', + 'filename', 'title', 'typology', 'image', @@ -82,6 +84,7 @@ class PartSerializer(serializers.ModelSerializer): def create(self, data): document = Document.objects.get(pk=self.context["view"].kwargs["document_pk"]) data['document'] = document + data['original_filename'] = data['image'].name obj = super().create(data) # generate card thumbnail right away since we need it get_thumbnailer(obj.image).get_thumbnail(settings.THUMBNAIL_ALIASES['']['card']) @@ -111,7 +114,7 @@ class LineTranscriptionSerializer(serializers.ModelSerializer): class LineSerializer(serializers.ModelSerializer): class Meta: model = Line - fields = ('pk', 'order', 'block', 'box') + fields = ('pk', 'document_part', 'order', 'block', 'box') def create(self, validated_data): instance = super().create(validated_data) @@ -128,7 +131,7 @@ class DetailedLineSerializer(LineSerializer): transcriptions = LineTranscriptionSerializer(many=True, required=False) class Meta(LineSerializer.Meta): - fields = LineSerializer.Meta.fields + ('document_part', 'block', 'transcriptions',) + fields = LineSerializer.Meta.fields + ('transcriptions',) class PartDetailSerializer(PartSerializer): diff --git a/app/apps/api/tests.py b/app/apps/api/tests.py index 43b34c83cf01d462695c285ef90c4cb48346efb4..e0e0dc5ca148d6f4fefa898583add20805433d0a 100644 --- a/app/apps/api/tests.py +++ b/app/apps/api/tests.py @@ -188,7 +188,7 @@ class LineViewSetTestCase(CoreFactoryTestCase): uri = reverse('api:line-list', kwargs={'document_pk': self.part.document.pk, 'part_pk': self.part.pk}) - with self.assertNumQueries(13): + with self.assertNumQueries(12): resp = self.client.post(uri, { 'document_part': self.part.pk, 'box': '[10, 10, 50, 50]' diff --git a/app/apps/api/views.py b/app/apps/api/views.py index d78bda885a404b7c01547fca9d0c8732a80a16ac..5d4c634dc81b10eacc3b2ce0f08ca9dff0622016 100644 --- a/app/apps/api/views.py +++ b/app/apps/api/views.py @@ -1,5 +1,8 @@ -from django.db.models import Prefetch -from django.shortcuts import render +import itertools + +from django.db.models import Prefetch, Count +from django.http import StreamingHttpResponse +from django.template import loader from django.utils.text import slugify from rest_framework.decorators import action @@ -36,7 +39,6 @@ class DocumentViewSet(ModelViewSet): try: form.process() except ParseError as e: - return error("Incorrectly formated file, couldn't parse it.") return Response({'status': 'ok'}) else: @@ -53,16 +55,7 @@ class DocumentViewSet(ModelViewSet): return Response({'status': 'already canceled'}, status=400) @action(detail=True, methods=['get']) - def export(self, request, pk=None): - def fetch_by_batch(queryset, start=0, size=200): - while True: - results = queryset[start:start+size] - for result in results: - yield result - if len(results) < size: - break - start += size - + def export(self, request, pk=None): format_ = request.GET.get('as', 'text') try: transcription = Transcription.objects.get( @@ -70,37 +63,53 @@ class DocumentViewSet(ModelViewSet): except Transcription.DoesNotExist: return Response({'error': "Object 'transcription' is required."}, status=status.HTTP_400_BAD_REQUEST) self.object = self.get_object() - if format_ == 'text': template = 'core/export/simple.txt' content_type = 'text/plain' extension = 'txt' lines = (LineTranscription.objects.filter(transcription=transcription) - .order_by('line__document_part', 'line__document_part__order', 'line__order') - .select_related('line', 'line__document_part', 'line__block')) - context = {'lines': fetch_by_batch(lines)} + .order_by('line__document_part', 'line__document_part__order', 'line__order')) + response = StreamingHttpResponse(['%s\n' % line.content for line in lines], + content_type=content_type) elif format_ == 'alto': template = 'core/export/alto.xml' content_type = 'text/xml' extension = 'xml' - lines = (Line.objects - .filter(document_part__document=pk) - .select_related('document_part', 'block') - .order_by('document_part', 'block', 'order') - .prefetch_related( - Prefetch('transcriptions', - to_attr='transcription', - queryset=LineTranscription.objects.filter(transcription=transcription)))) - context = {'lines': fetch_by_batch(lines)} + document = self.get_object() + part_pks = document.parts.values_list('pk', flat=True) + start = loader.get_template('core/export/alto_start.xml').render() + end = loader.get_template('core/export/alto_end.xml').render() + part_tmpl = loader.get_template('core/export/alto_part.xml') + response = StreamingHttpResponse(itertools.chain([start], + [part_tmpl.render({ + 'part': self.get_part_data(pk, transcription), + 'counter': i}) + for i, pk in enumerate(part_pks)], + [end]), + content_type=content_type) else: return Response({'error': 'Invalid format.'}, status=status.HTTP_400_BAD_REQUEST) - response = render(request, template, - context=context, - content_type=content_type) + response['Content-Disposition'] = 'attachment; filename="export-%s-%s.%s"' % ( slugify(self.object.name), datetime.now().isoformat()[:16], extension) return response + + def get_part_data(self, part_pk, transcription): + return (DocumentPart.objects + .prefetch_related( + Prefetch('blocks', + to_attr='orphan_blocks', + queryset=(Block.objects.annotate(num_lines=Count("line")) + .filter(num_lines=0))), + Prefetch('lines', + queryset=(Line.objects.all() + .select_related('block') + .prefetch_related( + Prefetch('transcriptions', + to_attr='transcription', + queryset=LineTranscription.objects.filter(transcription=transcription)))))) + .get(pk=part_pk)) class PartViewSet(ModelViewSet): @@ -151,6 +160,12 @@ class LineViewSet(ModelViewSet): queryset = Line.objects.all().select_related('block').prefetch_related('transcriptions__transcription') serializer_class = DetailedLineSerializer + def get_serializer_class(self): + if self.action in ['retrieve', 'list']: + return DetailedLineSerializer + else: # create + return LineSerializer + class LargeResultsSetPagination(PageNumberPagination): page_size = 100 diff --git a/app/apps/core/migrations/0022_documentpart_original_filename.py b/app/apps/core/migrations/0022_documentpart_original_filename.py new file mode 100644 index 0000000000000000000000000000000000000000..8d10e3989b56b311f9930f59b2e90714eaf301ac --- /dev/null +++ b/app/apps/core/migrations/0022_documentpart_original_filename.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.4 on 2019-05-13 13:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0021_auto_20190507_1304'), + ] + + operations = [ + migrations.AddField( + model_name='documentpart', + name='original_filename', + field=models.CharField(blank=True, max_length=1024), + ), + ] diff --git a/app/apps/core/models.py b/app/apps/core/models.py index 675dc89eed669868d34736de500a8c7b96a03db5..bb62eaf1f39e553fdd666ed9f0e89204f1d1ffa1 100644 --- a/app/apps/core/models.py +++ b/app/apps/core/models.py @@ -228,6 +228,7 @@ class DocumentPart(OrderedModel): """ name = models.CharField(max_length=512, blank=True) image = models.ImageField(upload_to=document_images_path) + original_filename = models.CharField(max_length=1024, blank=True) source = models.CharField(max_length=1024, blank=True) bw_backend = models.CharField(max_length=128, default='kraken') bw_image = models.ImageField(upload_to=document_images_path, @@ -290,6 +291,13 @@ class DocumentPart(OrderedModel): @property def segmented(self): return self.lines.count() > 0 + + def make_external_id(self): + return 'eSc_page_%d' % self.pk + + @property + def filename(self): + return self.original_filename or os.path.split(self.image.path)[1] def calculate_progress(self): if self.workflow_state < self.WORKFLOW_STATE_TRANSCRIBING: @@ -659,12 +667,12 @@ class Block(OrderedModel, models.Model): document_part = models.ForeignKey(DocumentPart, on_delete=models.CASCADE, related_name='blocks') order_with_respect_to = 'document_part' - + external_id = models.CharField(max_length=128, blank=True, null=True) class Meta(OrderedModel.Meta): pass - + @property def width(self): return self.box[2] - self.box[0] @@ -674,7 +682,7 @@ class Block(OrderedModel, models.Model): return self.box[3] - self.box[1] def make_external_id(self): - return self.external_id or 'textblock_%d' % self.pk + return self.external_id or 'eSc_textblock_%d' % self.pk class Line(OrderedModel): # Versioned, @@ -706,9 +714,9 @@ class Line(OrderedModel): # Versioned, @property def height(self): return self.box[3] - self.box[1] - + def make_external_id(self): - return self.external_id or 'line_%d' % self.pk + return self.external_id or 'eSc_line_%d' % self.pk class Transcription(models.Model): diff --git a/app/apps/core/static/js/edit.js b/app/apps/core/static/js/edit.js index ee40e638790b496ccc033af0a4f2a45617e4c78f..465ab748bdb17b9707edfc8c15b7903b5f13fb15 100644 --- a/app/apps/core/static/js/edit.js +++ b/app/apps/core/static/js/edit.js @@ -38,8 +38,7 @@ $(document).ready(function() { else $('a#next-part').hide(); if (data.image && data.image.uri) { - let imagename = data.image.uri.split('/').slice(-1)[0]; - $('#part-name').html(data.title).attr('title', imagename); + $('#part-name').html(data.title).attr('title', '<'+data.filename+'>'); } // set the 'image' tab btn to select the corresponding image @@ -59,7 +58,8 @@ $(document).ready(function() { panels[key].reset(); } }); - + + // previous and next buttons $('a#prev-part, a#next-part').click(function(ev) { ev.preventDefault(); var pk = $(ev.target).parents('a').data('target'); diff --git a/app/apps/core/static/js/image_cards.js b/app/apps/core/static/js/image_cards.js index 2b3dc86dbb3df980f7cd786f256989fafa31ea9c..9ea66626229c6ece76e986f4d51018ec87a68624 100644 --- a/app/apps/core/static/js/image_cards.js +++ b/app/apps/core/static/js/image_cards.js @@ -36,6 +36,7 @@ class partCard { this.title = part.title; this.typology = part.typology; this.image = part.image; + this.filename = part.filename; this.bw_image = part.bw_image; this.workflow = part.workflow; this.task_ids = {}; // helps preventing card status race conditions @@ -55,8 +56,7 @@ class partCard { // fill template $new.attr('id', $new.attr('id').replace('{pk}', this.pk)); this.updateThumbnail(); - let filename = this.image.uri.split('/').splice(-1)[0]; - $('img.card-img-top', $new).attr('title', this.title + '\n<' + filename +'>'); + $('img.card-img-top', $new).attr('title', this.title + '\n<' + this.filename +'>'); $new.attr('draggable', true); $('img', $new).attr('draggable', false); @@ -175,7 +175,7 @@ class partCard { updateThumbnail() { let uri, img = $('img.card-img-top', this.$element); - if (this.image.thumbnails['card'] != undefined) { + if (this.image.thumbnails && this.image.thumbnails['card'] != undefined) { uri = this.image.thumbnails['card']; } else { uri = this.image.uri; diff --git a/app/apps/core/static/js/panels/binarization.js b/app/apps/core/static/js/panels/binarization.js index 1e73727d6335716ae6a8b001178155564b7b9579..152130da33fe3ff433848606db1f656f5753cf8a 100644 --- a/app/apps/core/static/js/panels/binarization.js +++ b/app/apps/core/static/js/panels/binarization.js @@ -4,15 +4,14 @@ class BinarizationPanel { this.opened = opened | false; this.$container = $('.img-container', this.$panel); zoom.register(this.$container); + $('.img-container img', this.$panel).on('load', $.proxy(function() { + zoom.refresh(); + }, this)); } load(part) { this.part = part; - $('.img-container img', this.$panel).on('load', $.proxy(function() { - zoom.refresh(); - }, this)); - if (this.part.bw_image) { if (this.part.bw_image.thumbnails) { $('.img-container img', this.$panel).attr('src', this.part.bw_image.thumbnails.large); @@ -42,7 +41,5 @@ class BinarizationPanel { else this.open(); } - reset() { - zoom.refresh(); - } + reset() {} } diff --git a/app/apps/core/static/js/panels/source.js b/app/apps/core/static/js/panels/source.js index 77758c996c631dae668850853a989cc6752c0ba4..13d2790265594ec26b83b7fa03906007d8c65225 100644 --- a/app/apps/core/static/js/panels/source.js +++ b/app/apps/core/static/js/panels/source.js @@ -4,15 +4,14 @@ class SourcePanel { this.opened = opened | false; this.$container = $('.img-container', this.$panel); zoom.register(this.$container); + $('.img-container img', this.$panel).on('load', $.proxy(function(data) { + zoom.refresh(); + }, this)); } load(part) { this.part = part; - $('.img-container img', this.$panel).on('load', $.proxy(function() { - zoom.refresh(); - }, this)); - if (this.part.image.thumbnails) { $('.img-container img', this.$panel).attr('src', this.part.image.thumbnails.large); } else { @@ -38,7 +37,5 @@ class SourcePanel { else this.open(); } - reset() { - zoom.refresh(); - } + reset() {} } diff --git a/app/apps/core/static/js/panels/transcription.js b/app/apps/core/static/js/panels/transcription.js index 8abe94bc23a7af586defe04e4e31ac35daef65ef..a86ca04e82bfea0423c072d82a8a72ee893e8df5 100644 --- a/app/apps/core/static/js/panels/transcription.js +++ b/app/apps/core/static/js/panels/transcription.js @@ -326,6 +326,7 @@ class TranscriptionPanel{ }); if (this.opened) this.open(); + zoom.register(this.$container, true); } addLine(line, ratio) { @@ -350,6 +351,10 @@ class TranscriptionPanel{ }, this)); }, this); getNext(1); + + $('.zoom-container', this.$container).css({ + width: this.part.image.size[0]*this.ratio, + height: this.part.image.size[1]*this.ratio}); } load(part) { @@ -363,7 +368,6 @@ class TranscriptionPanel{ this.addLine(this.part.lines[i]); } this.loadTranscriptions(); - zoom.register(this.$container, true); } open() { @@ -371,7 +375,7 @@ class TranscriptionPanel{ this.$panel.show(); Cookies.set('trans-panel-open', true); } - + close() { this.opened = false; this.$panel.hide(); @@ -389,6 +393,10 @@ class TranscriptionPanel{ for (var i=0; i<this.lines.length; i++) { this.lines[i].reset(); } + + $('.zoom-container', this.$container).css({ + width: this.part.image.size[0]*this.ratio, + height: this.part.image.size[1]*this.ratio}); } } } diff --git a/app/apps/core/tests/export.py b/app/apps/core/tests/export.py index 951d1102bbaf5b6ddefef5eb60b40da65d73c2bf..ed59c7e6442667b138ef87ba20ad8d1ef4c456b3 100644 --- a/app/apps/core/tests/export.py +++ b/app/apps/core/tests/export.py @@ -1,17 +1,17 @@ from django.urls import reverse from django.test import TestCase -from core.models import Line, LineTranscription +from core.models import Line, LineTranscription, Block from core.tests.factory import CoreFactory class DocumentExportTestCase(TestCase): def setUp(self): - factory = CoreFactory() - self.trans = factory.make_transcription() + self.factory = CoreFactory() + self.trans = self.factory.make_transcription() self.user = self.trans.document.owner # shortcut for i in range(1, 3): - part = factory.make_part(name='part %d' % i, + part = self.factory.make_part(name='part %d' % i, document=self.trans.document) for j in range(1, 4): l = Line.objects.create(document_part=part, @@ -27,8 +27,39 @@ class DocumentExportTestCase(TestCase): resp = self.client.get(reverse('api:document-export', kwargs={'pk': self.trans.document.pk}) + '?transcription=' + str(self.trans.pk)) - self.assertEqual(resp.content.decode(), "\nline 1:1\nline 1:2\nline 1:3\n-\nline 2:1\nline 2:2\nline 2:3\n") + self.assertEqual(''.join([c.decode() for c in resp.streaming_content]), + "line 1:1\nline 1:2\nline 1:3\nline 2:1\nline 2:2\nline 2:3\n") + + def test_alto(self): + self.client.force_login(self.user) + with self.assertNumQueries(16): # should be 8 + 4*part + resp = self.client.get(reverse('api:document-export', + kwargs={'pk': self.trans.document.pk,}) + + '?transcription=%d&as=alto' % self.trans.pk) + content = list(resp.streaming_content) + self.assertEqual(len(content), 4) # start + 2 part + end + + def test_alto_qs_scaling(self): + for i in range(4, 20): + part = self.factory.make_part(name='part %d' % i, + document=self.trans.document) + block = Block.objects.create(document_part=part, box=(0,0,1,1)) + for j in range(1, 4): + l = Line.objects.create(document_part=part, + block=block, + box=(0,0,1,1)) + LineTranscription.objects.create( + line=l, + transcription=self.trans, + content='line %d:%d' % (i,j)) + self.client.force_login(self.user) + with self.assertNumQueries(80): + resp = self.client.get(reverse('api:document-export', + kwargs={'pk': self.trans.document.pk,}) + + '?transcription=%d&as=alto' % self.trans.pk) + self.assertEqual(resp.status_code, 200) + def test_invalid(self): self.client.force_login(self.user) resp = self.client.get(reverse('api:document-export', diff --git a/app/apps/imports/parsers.py b/app/apps/imports/parsers.py index 578f08d002ecd765301d643e43f86d38aa1c0680..0ad08af33f9e9d131fe475cdb68a4daff3a00119 100644 --- a/app/apps/imports/parsers.py +++ b/app/apps/imports/parsers.py @@ -1,7 +1,8 @@ -import time +from lxml import etree import os.path import requests -from lxml import etree +import time +import uuid from django.core.files.base import ContentFile from django.db import transaction @@ -79,18 +80,36 @@ class XMLParser(): part.lines.all().delete() for block in self.find(page, self.TAGS['block']): # Note: don't use get_or_create to avoid a update query - attrs = {'document_part': part, - 'external_id': block.get('ID')} - try: - b = Block.objects.get(**attrs) - except Block.DoesNotExist: - b = Block(**attrs) - b.box = self.block_bbox(block) - b.save() + id_ = block.get('ID') + if id_ and id_.startswith('eSc_dummyblock_'): + block_ = None + else: + try: + assert id_ and id_.startswith('eSc_textblock_') + attrs = {'pk': int(id_[len('eSc_textblock_'):])} + except (ValueError, AssertionError, TypeError): + attrs = {'document_part': part, + 'external_id': id_} + try: + block_ = Block.objects.get(**attrs) + except Block.DoesNotExist: + block_ = Block(**attrs) + try: + block_.box = self.block_bbox(block) + except TypeError: # probably a dummy block + block = None + else: + block_.save() + for line in self.find(block, self.TAGS['line']): - attrs = {'document_part': part, - 'block': b, - 'external_id': line.get('ID')} + id_ = line.get('ID') + try: + assert id_ and id_.startswith('eSc_line_') + attrs = {'pk': int(id_[len('eSc_line_'):])} + except (ValueError, AssertionError, TypeError): + attrs = {'document_part': part, + 'block': block_, + 'external_id': line.get('ID')} try: l = Line.objects.get(**attrs) except Line.DoesNotExist: @@ -230,8 +249,9 @@ class IIIFManifesParser(): ) if 'label' in resource: part.name = resource['label'] - name = '%d_%s' % (i, url.split('/')[-1]) - part.image.save(name, ContentFile(r.content)) + name = '%d_%s_%s' % (i, uuid.uuid4().hex[:5], url.split('/')[-1]) + part.original_filename = name + part.image.save(name, ContentFile(r.content), save=False) part.save() yield part time.sleep(0.1) # avoid being throttled diff --git a/app/apps/imports/tests.py b/app/apps/imports/tests.py index 92be5bb7a0fa062e2652a79cba9c430e5f0422a3..2ae6fda62886bf0d61c1aafba5c16ccb7888b0d9 100644 --- a/app/apps/imports/tests.py +++ b/app/apps/imports/tests.py @@ -42,7 +42,7 @@ class XmlImportTestCase(CoreFactoryTestCase): self.assertEqual(self.part1.lines.first().box, [160, 771, 220, 799]) self.assertEqual(self.part1.lines.first().transcriptions.first().content, 'This is a test') self.assertEqual(self.part2.blocks.count(), 0) - self.assertEqual(self.part2.lines.count(), 0) + self.assertEqual(self.part2.lines.count(), 0) def test_alto_multi(self): uri = reverse('api:document-imports', kwargs={'pk': self.document.pk}) diff --git a/app/escriptorium/static/css/escriptorium.css b/app/escriptorium/static/css/escriptorium.css index f7b5fe68d69c870bbeea4f6dd9d2a4ded31ec684..ffdc1da378e0da178f95bb925d49431f86faf73c 100644 --- a/app/escriptorium/static/css/escriptorium.css +++ b/app/escriptorium/static/css/escriptorium.css @@ -289,6 +289,10 @@ form.inline-form { #binar-tools, #seg-tools, #trans-panel, #trans-tools { display: none; } +#img-panel, #binar-panel, #seg-panel, #trans-panel { + min-height: calc(100vh - 200px); +} + i.panel-icon { vertical-align: top; } @@ -303,6 +307,7 @@ i.panel-icon { } .zoom-container { transform-origin: 0 0; + box-shadow: 1px 0 4px 1px rgba(0,0,0,0.1); } #zoom-range { height: 100px; @@ -310,12 +315,15 @@ i.panel-icon { #part-trans, #part-img { overflow: hidden; - /* position: absolute; */ width: 100%; height: 100%; transform-origin:top left; } +#trans-panel .zoom-container { + position: absolute; +} + .overlay { top: 0; left: 0; diff --git a/app/escriptorium/static/js/wheelzoom.js b/app/escriptorium/static/js/wheelzoom.js index fbf62f81aae635f3c350f08171966df46fe49024..b4caed3b740631dcb640533313c07d862856fff0 100644 --- a/app/escriptorium/static/js/wheelzoom.js +++ b/app/escriptorium/static/js/wheelzoom.js @@ -4,7 +4,7 @@ class WheelZoom { constructor(options) { this.options = options || {}; var defaults = { - factor: 0.2, + factor: 0.1, min_scale: 1, max_scale: null, initial_scale: 1, @@ -29,13 +29,18 @@ class WheelZoom { register(container, mirror) { var target = container.children().first(); - this.size = {w:target.width() * this.initial_scale, h:target.height() * this.initial_scale}; + this.size = {w:target.width() * this.scale, h:target.height() * this.scale}; + this.min_scale = this.options.min_scale || Math.min( + $(window).width() / (this.size.w * this.initial_scale) * 0.9, + $(window).height() / (this.size.h * this.initial_scale) * 0.9); target.css({transformOrigin: '0 0', transition: 'transform 0.3s'}); if (mirror !== true) { target.css({cursor: 'zoom-in'}); container.on("mousewheel DOMMouseScroll", $.proxy(this.scrolled, this)); container.on('mousedown', $.proxy(this.draggable, this)); + } else { + container.addClass('mirror'); } this.events.on('wheelzoom.reset', $.proxy(this.reset, this)); this.events.on('wheelzoom.refresh', $.proxy(this.refresh, this)); @@ -100,17 +105,23 @@ class WheelZoom { updateStyle() { // Make sure the slide stays in its container area when zooming in/out - if (this.scale > 1) { + let container = this.getVisibleContainer(); + if (this.size.w*this.scale > container.width()) { if(this.pos.x > 0) { this.pos.x = 0; } - if(this.pos.x+this.size.w*this.scale < this.size.w) { this.pos.x = this.size.w - this.size.w*this.scale; } - if(this.pos.y > 0) { this.pos.y = 0; } - if(this.pos.y+this.size.h*this.scale < this.size.h) { this.pos.y = this.size.h - this.size.h*this.scale; } + if(this.pos.x+this.size.w*this.scale < container.width()) { this.pos.x = container.width() - this.size.w*this.scale; } } else { if(this.pos.x < 0) { this.pos.x = 0; } - if(this.pos.x+this.size.w*this.scale > this.size.w) { this.pos.x = -this.size.w*(this.scale-1); } + if(this.pos.x+this.size.w*this.scale > container.width()) { this.pos.x = container.width() - this.size.w*this.scale; } + } + + if (this.size.h*this.scale > container.height()) { + if(this.pos.y > 0) { this.pos.y = 0; } + if(this.pos.y+this.size.h*this.scale < container.height()) { this.pos.y = container.height() - this.size.h*this.scale; } + } else { if(this.pos.y < 0) { this.pos.y = 0; } - if(this.pos.y+this.size.h*this.scale > this.size.h) { this.pos.y = -this.size.h*(this.scale-1); } + if(this.pos.y+this.size.h*this.scale > container.height()) { this.pos.y = container.height() - this.size.h*this.scale; } } + // apply scale first for transition effect this.targets.forEach($.proxy(function(e, i) { @@ -122,12 +133,13 @@ class WheelZoom { } getVisibleContainer() { - return this.containers.find(function(e) { return e.is(':visible') && e.height() != 0;}); + return this.containers.find(function(e) { return e.is(':visible:not(.mirror)') && e.height() != 0;}); } refresh() { let container = this.getVisibleContainer(); - this.size = {w: container.width(), h: container.height()}; + var target = container.children().first(); + this.size = {w:target.width(), h:target.height()}; this.min_scale = this.options.min_scale || Math.min( $(window).width() / (this.size.w * this.initial_scale) * 0.9, $(window).height() / (this.size.h * this.initial_scale) * 0.9); diff --git a/app/escriptorium/templates/core/export/alto_end.xml b/app/escriptorium/templates/core/export/alto_end.xml new file mode 100644 index 0000000000000000000000000000000000000000..ff22cdf1314b5fc8984bdec5f625aa1c2f6a31f6 --- /dev/null +++ b/app/escriptorium/templates/core/export/alto_end.xml @@ -0,0 +1 @@ +</Layout></alto> diff --git a/app/escriptorium/templates/core/export/alto_part.xml b/app/escriptorium/templates/core/export/alto_part.xml new file mode 100644 index 0000000000000000000000000000000000000000..428f14ff2b44134dbfa5d9b0939dee20db7272ea --- /dev/null +++ b/app/escriptorium/templates/core/export/alto_part.xml @@ -0,0 +1,12 @@ + <Page PHYSICAL_IMG_NR="{{ counter }}" WIDTH="{{ part.image.width }}" HEIGHT="{{ part.image.height }}" ID="{{part.make_external_id}}"> + <PrintSpace HPOS="0" VPOS="0" WIDTH="{{ part.image.width }}" HEIGHT="{{ part.image.height }}"> + {% regroup part.lines.all by block as part_blocks %}{% for block in part_blocks %}{% ifchanged block.grouper %} + <TextBlock {% if block.grouper %}HPOS="{{ block.grouper.box.0|default:0 }}" VPOS="{{ block.grouper.box.1|default:0 }}" WIDTH="{{ block.grouper.width|default:0 }}" HEIGHT="{{ block.grouper.height|default:0 }}" ID="{{ block.grouper.make_external_id }}"{% else %}ID="eSc_dummyblock_{{ block.list.0.pk }}"{% endif %}>{% endifchanged %} + {% for line in block.list %}<TextLine ID="{{ line.make_external_id }}" HPOS="{{ line.box.0 }}" VPOS="{{ line.box.1 }}" WIDTH="{{ line.width }}" HEIGHT="{{ line.height }}"> + <String CONTENT="{{ line.transcription.0.content }}" HPOS="{{ line.box.0 }}" VPOS="{{ line.box.1 }}" WIDTH="{{ line.width }}" HEIGHT="{{ line.height }}"></String> + </TextLine>{% endfor %} + </TextBlock>{% endfor %} + {% for block in part.orphan_blocks %}<TextBlock HPOS="{{ block.box.0 }}" VPOS="{{ block.box.1 }}" WIDTH="{{ block.width }}" HEIGHT="{{ block.height }}" ID="{{ block.make_external_id }}"> + </TextBlock>{% endfor %} + </PrintSpace> + </Page> diff --git a/app/escriptorium/templates/core/export/alto_start.xml b/app/escriptorium/templates/core/export/alto_start.xml new file mode 100644 index 0000000000000000000000000000000000000000..61435d57ee13dda110dfa058a36aac4553be04e8 --- /dev/null +++ b/app/escriptorium/templates/core/export/alto_start.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<alto xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://www.loc.gov/standards/alto/ns-v4#" + xsi:schemaLocation="http://www.loc.gov/standards/alto/ns-v4# http://www.loc.gov/standards/alto/v4/alto-4-0.xsd"> +<Layout> diff --git a/app/escriptorium/templates/core/home.html b/app/escriptorium/templates/core/home.html index 2f88e35ba7714544c2ab602da411f89790332e64..8128eb52f14605db73870b77bf3a323888682977 100644 --- a/app/escriptorium/templates/core/home.html +++ b/app/escriptorium/templates/core/home.html @@ -7,6 +7,6 @@ {% block body %} <div class="jumbotron"> <h1>eScriptorium</h1> -<p>A project providing digital recognition of handwritten documents using machine learning techniques<p> +<p>{% trans "A project providing digital recognition of handwritten documents using machine learning techniques." %}<p> </div> {% endblock %}