Mediafile pdf presentation with angular-pdf and pdf.js (Fixes #1664).

Thanks to André Böhlke for contribution core functionality!
Some additional template improvements by Emanuel.
This commit is contained in:
André Böhlke 2016-01-27 15:09:49 +07:00 committed by Emanuel Schuetze
parent 3ba93c2352
commit d3ed15db29
12 changed files with 239 additions and 15 deletions

View File

@ -31,9 +31,10 @@
"ckeditor": "~4.5.4", "ckeditor": "~4.5.4",
"angular-ckeditor": "~1.0.0", "angular-ckeditor": "~1.0.0",
"roboto-condensed": "~0.3.0", "roboto-condensed": "~0.3.0",
"open-sans-fontface": "https://github.com/OpenSlides/open-sans.git#1.4.2.post1" "open-sans-fontface": "https://github.com/OpenSlides/open-sans.git#1.4.2.post1",
"angular-pdf": "~1.3.0"
}, },
"resolutions": { "resolutions": {
"angular": "^1.2.x" "angular": ">=1.4.9 <1.5"
} }
} }

View File

@ -21,6 +21,7 @@ var argv = require('yargs').argv,
mainBowerFiles = require('main-bower-files'), mainBowerFiles = require('main-bower-files'),
minifyCSS = require('gulp-minify-css'), minifyCSS = require('gulp-minify-css'),
path = require('path'), path = require('path'),
rename = require("gulp-rename"),
through = require('through2'), through = require('through2'),
uglify = require('gulp-uglify'), uglify = require('gulp-uglify'),
vsprintf = require('sprintf-js').vsprintf; vsprintf = require('sprintf-js').vsprintf;
@ -69,6 +70,13 @@ gulp.task('ckeditor', function () {
.pipe(gulp.dest(path.join(output_directory, 'ckeditor'))); .pipe(gulp.dest(path.join(output_directory, 'ckeditor')));
}); });
// Extra task only for pdfjs
gulp.task('pdfjs', function () {
return gulp.src(path.join('bower_components', 'pdfjs-dist', 'build', 'pdf.worker.js'))
.pipe(rename(path.join('openslides-libs.worker.js')))
.pipe(gulp.dest(path.join(output_directory, 'js')));
});
// Compiles translation files (*.po) to *.json and saves them in the directory // Compiles translation files (*.po) to *.json and saves them in the directory
// openslides/static/i18n/. // openslides/static/i18n/.
gulp.task('translations', function () { gulp.task('translations', function () {
@ -80,7 +88,7 @@ gulp.task('translations', function () {
}); });
// Gulp default task. Runs all other tasks before. // Gulp default task. Runs all other tasks before.
gulp.task('default', ['js-libs', 'css-libs', 'fonts-libs', 'ckeditor', 'translations'], function () {}); gulp.task('default', ['js-libs', 'css-libs', 'fonts-libs', 'ckeditor', 'pdfjs', 'translations'], function () {});
/** /**

View File

@ -10,6 +10,7 @@ angular.module('OpenSlidesApp.core', [
'ngSanitize', // TODO: only use this in functions that need it. 'ngSanitize', // TODO: only use this in functions that need it.
'ui.bootstrap', 'ui.bootstrap',
'ui.tree', 'ui.tree',
'pdf'
]) ])
.config([ .config([

View File

@ -7,6 +7,7 @@
<link rel="stylesheet" href="static/css/projector.css"> <link rel="stylesheet" href="static/css/projector.css">
<link rel="icon" href="/static/img/favicon.png"> <link rel="icon" href="/static/img/favicon.png">
<script src="static/js/openslides-libs.js"></script> <script src="static/js/openslides-libs.js"></script>
<script src="static/js/openslides-libs.worker.js"></script>
<style type="text/css"> <style type="text/css">
#header, #footer { #header, #footer {

View File

@ -2,6 +2,7 @@ import mimetypes
from django.conf import settings from django.conf import settings
from django.db import models as dbmodels from django.db import models as dbmodels
from PyPDF2 import PdfFileReader
from ..utils.rest_api import FileField, ModelSerializer, SerializerMethodField from ..utils.rest_api import FileField, ModelSerializer, SerializerMethodField
from .models import Mediafile from .models import Mediafile
@ -17,10 +18,14 @@ class AngularCompatibleFileField(FileField):
def to_representation(self, value): def to_representation(self, value):
if value is None: if value is None:
return None return None
return { filetype = mimetypes.guess_type(value.path)[0]
result = {
'name': value.name, 'name': value.name,
'type': mimetypes.guess_type(value.path)[0] 'type': filetype
} }
if filetype == 'application/pdf':
result['pages'] = PdfFileReader(open(value.path, 'rb')).getNumPages()
return result
class MediafileSerializer(ModelSerializer): class MediafileSerializer(ModelSerializer):

View File

@ -2,6 +2,57 @@
'use strict'; 'use strict';
angular.module('OpenSlidesApp.mediafiles.projector', ['OpenSlidesApp.mediafiles']); angular.module('OpenSlidesApp.mediafiles.projector', ['OpenSlidesApp.mediafiles'])
}()); .config([
'slidesProvider',
function(slidesProvider) {
slidesProvider.registerSlide('mediafiles/mediafile', {
template: 'static/templates/mediafiles/slide_mediafile.html'
});
}
])
.controller('SlideMediafileCtrl', [
'$scope',
'Mediafile',
function($scope, Mediafile) {
// load mediafile object
var mediafile = Mediafile.find($scope.element.id);
mediafile.then(function(mediafile) {
$scope.pdfName = mediafile.title;
$scope.pdfUrl = mediafile.mediafileUrl;
})
// get page from projector
$scope.page = $scope.element.page;
$scope.scroll = 0;
function updateScale() {
if($scope.element.pageFit) {
$scope.scale = 'page-fit';
} else {
$scope.scale = $scope.element.scale;
}
}
$scope.$watch(function() {
return $scope.element.scale;
}, updateScale);
updateScale();
$scope.getNavStyle = function(scroll) {
if (scroll > 100) {
return 'pdf-controls fixed';
} else {
return 'pdf-controls';
}
};
$scope.onLoad = function() {
$scope.loading = '';
};
}
]);
})();

View File

@ -51,7 +51,8 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
'Mediafile', 'Mediafile',
'MediafileForm', 'MediafileForm',
'User', 'User',
function($scope, $http, ngDialog, Mediafile, MediafileForm, User) { 'Projector',
function($scope, $http, ngDialog, Mediafile, MediafileForm, User, Projector) {
Mediafile.bindAll({}, $scope, 'mediafiles'); Mediafile.bindAll({}, $scope, 'mediafiles');
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
@ -59,6 +60,20 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
$scope.sortColumn = 'title'; $scope.sortColumn = 'title';
$scope.filterPresent = ''; $scope.filterPresent = '';
$scope.reverse = false; $scope.reverse = false;
function updatePresentedMediafiles() {
var projectorElements = _.map(Projector.get(1).elements, function(element) { return element });
$scope.presentedMediafiles = _.filter(projectorElements, function (element) {
return element.name === 'mediafiles/mediafile';
});
}
$scope.$watch(function() {
return Projector.get(1).elements;
}, updatePresentedMediafiles);
updatePresentedMediafiles();
// function to sort by clicked column // function to sort by clicked column
$scope.toggleSort = function ( column ) { $scope.toggleSort = function ( column ) {
if ( $scope.sortColumn === column ) { if ( $scope.sortColumn === column ) {
@ -111,6 +126,105 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
$scope.delete = function (mediafile) { $scope.delete = function (mediafile) {
Mediafile.destroy(mediafile.id); Mediafile.destroy(mediafile.id);
}; };
// ** PDF presentation functions **/
// show document on projector
$scope.showPdf = function (mediafile) {
var postUrl,
data;
if ($scope.presentedMediafiles.length > 0) {
// update first mediafile, at the moment there should not be more
var uuid = $scope.presentedMediafiles[0].uuid;
postUrl = '/rest/core/projector/1/update_elements/';
data = {};
data[uuid] = {
id: mediafile.id,
numPages: mediafile.mediafile.pages,
page: 1,
pageFit: true,
scale: 1,
visible: true
};
} else {
postUrl = '/rest/core/projector/1/prune_elements/';
data = [{
name: 'mediafiles/mediafile',
id: mediafile.id,
numPages: mediafile.mediafile.pages,
visible: true,
pageFit: true,
scale: 1,
page: 1
}];
}
$http.post(postUrl, data);
};
function sendMediafileCommand(data) {
var mediafileElement = getCurrentlyPresentedMediafile();
var updateData = _.extend({}, mediafileElement);
_.extend(updateData, data);
var postData = {};
postData[mediafileElement.uuid] = updateData;
$http.post('/rest/core/projector/1/update_elements/', postData);
}
function getCurrentlyPresentedMediafile() {
return $scope.presentedMediafiles[0];
}
$scope.mediafileGoPrevious = function () {
var mediafileElement = getCurrentlyPresentedMediafile();
if (mediafileElement.page > 1) {
sendMediafileCommand({
page: parseInt(mediafileElement.page) - 1
});
}
};
$scope.mediafileGoNext = function () {
var mediafileElement = getCurrentlyPresentedMediafile();
if (mediafileElement.page < mediafileElement.numPages) {
sendMediafileCommand({
page: parseInt(mediafileElement.page) + 1
});
}
};
$scope.mediafileZoomIn = function () {
var mediafileElement = getCurrentlyPresentedMediafile();
sendMediafileCommand({
pageFit: false,
scale: parseFloat(mediafileElement.scale) + 0.2
});
};
$scope.mediafileFit = function () {
sendMediafileCommand({
pageFit: true
});
};
$scope.mediafileZoomOut = function () {
var mediafileElement = getCurrentlyPresentedMediafile();
sendMediafileCommand({
pageFit: false,
scale: parseFloat(mediafileElement.scale) - 0.2
});
};
$scope.mediafileChangePage = function(pageNum) {
sendMediafileCommand({
pageToDisplay: pageNum
});
};
$scope.mediafileRotate = function () {
var rotation;
var currentRotation = $scope.mediafile.rotation;
if (currentRotation === 270) {
rotation = 0;
} else {
rotation = currentRotation + 90;
}
sendMediafileCommand({
rotation: rotation
});
};
} }
]) ])

View File

@ -60,6 +60,40 @@
</div> </div>
</div> </div>
<!-- mediafile pdf controls -->
<div os-perms="core.can_manage_projector" ng-show="presentedMediafiles.length">
<div ng-repeat="presentedMediafile in presentedMediafiles" class="well well-sm">
<h3 translate>PDF control elements</h3>
<!-- TODO: show filename / title of mediafile -->
<nav ng-class="getNavStyle(scroll)" class="form-inline">
<div class="btn-group">
<button class="btn btn-default" ng-click="mediafileGoPrevious()" title="{{ 'Previouse page' | translate }}">
<i class="fa fa-backward"></i>
</button>
<button class="btn btn-default" ng-click="mediafileGoNext()" title="{{ 'Next page' | translate }}">
<i class="fa fa-forward"></i>
</button>
</div>
<div class="btn-group">
<button class="btn btn-default" ng-click="mediafileZoomOut()" title="{{ 'Zoom out' | translate }}">
<i class="fa fa-search-minus"></i>
</button>
<button class="btn btn-default" ng-click="mediafileFit()" title="{{ 'Reset zoom' | translate }}">
100%
</button>
<button class="btn btn-default" ng-click="mediafileZoomIn()" title="{{ 'Zoom in' | translate }}">
<i class="fa fa-search-plus"></i>
</button>
</div>
<div class="input-group">
<span class="input-group-addon" translate>Page</span>
<input type="number" min=1 ng-model="presentedMediafile.page" class="form-control" style="width: 80px">
<span class="input-group-addon"><translate>of</translate> {{presentedMediafile.numPages}}</span>
</div>
</nav>
</div>
</div>
<div class="spacer-top-lg italic"> <div class="spacer-top-lg italic">
{{ mediafilesFiltered.length }} / {{ mediafilesFiltered.length }} /
{{ mediafiles.length }} {{ "files" | translate }}<span ng-if="(mediafiles|filter:{selected:true}).length > 0">, {{ mediafiles.length }} {{ "files" | translate }}<span ng-if="(mediafiles|filter:{selected:true}).length > 0">,
@ -110,14 +144,12 @@
class="animate-item" class="animate-item"
ng-class="{ 'activeline': mediafile.isProjected(), 'selected': mediafile.selected }"> ng-class="{ 'activeline': mediafile.isProjected(), 'selected': mediafile.selected }">
<!-- projector column --> <!-- projector column -->
<!-- TOOD: implement project pdf feature -->
<td ng-show="!isDeleteMode" <td ng-show="!isDeleteMode"
os-perms="core.can_manage_projector"> os-perms="core.can_manage_projector">
<a class="btn btn-default btn-sm" <a class="btn btn-default btn-sm"
ng-if="mediafile.mediafile.type == 'application/pdf'" ng-if="mediafile.mediafile.type == 'application/pdf'"
ng-class="{ 'btn-primary': mediafile.isProjected() }" ng-class="{ 'btn-primary': mediafile.isProjected() }"
ng-click="mediafile.project()" ng-click="showPdf(mediafile)"
ng-bootbox-alert="{{ 'Sorry, the function to project pdf files is not yet implemented.' | translate }}"
title="{{ 'Project mediafile' | translate }}"> title="{{ 'Project mediafile' | translate }}">
<i class="fa fa-video-camera"></i> <i class="fa fa-video-camera"></i>
</a> </a>

View File

@ -0,0 +1,8 @@
<div ng-controller="SlideMediafileCtrl" class="content">
<ng-pdf template-url="/static/templates/mediafiles/slide_mediafile_partial.html"
scale="page-fit"
ng-attr-scale="{{ scale }}"
ng-attr-page="{{ page }}"
debug="true">
</ng-pdf>
</div>

View File

@ -0,0 +1 @@
<canvas id="pdf-canvas" class="rotate0"></canvas>

View File

@ -7,13 +7,14 @@
"gulp-angular-gettext": "~2.1.0", "gulp-angular-gettext": "~2.1.0",
"gulp-concat": "~2.6.0", "gulp-concat": "~2.6.0",
"gulp-if": "~2.0.0", "gulp-if": "~2.0.0",
"gulp-jshint": "~2.0.0",
"gulp-minify-css": "~1.2.3", "gulp-minify-css": "~1.2.3",
"gulp-rename": "~1.2.2",
"gulp-uglify": "~1.5.1", "gulp-uglify": "~1.5.1",
"main-bower-files": "~2.11.1", "main-bower-files": "~2.11.1",
"yargs": "~3.32.0",
"po2json": "~0.4.1", "po2json": "~0.4.1",
"gulp-jshint": "~2.0.0", "sprintf-js": "~1.0.3",
"through2": "~2.0.0", "through2": "~2.0.0",
"sprintf-js": "~1.0.3" "yargs": "~3.32.0"
} }
} }

View File

@ -9,4 +9,5 @@ reportlab>=3.0,<3.3
roman>=2.0,<2.1 roman>=2.0,<2.1
setuptools>=2.2,<20.0 setuptools>=2.2,<20.0
sockjs-tornado>=1.0,<1.1 sockjs-tornado>=1.0,<1.1
Whoosh>=2.7,<2.8 Whoosh>=2.7.0,<2.8
PyPDF2==1.25.1