[ADD] website_slides: new website module to handle presentations, documents,

videos, infographics. Some kind of slideshare clone, to summarize.

Featuring :
- channel management: classify slides (presentations, videos, documents, ...)
and tune their access rights for visibility, uploading and downloading
- full branding new shiny website module !
- automatic fetch of youtube and google docs information based on URL
- statistics: views, likes, interfaced with messaging, ...
- embed mechanism to embed slides in other websites

Courtesy of RD India, notably Mantavya Gajjar, Ravi Gadhia,  Vijay Patel,
Pariket Trivedi
Courtesy of Use Merge Team, notably Jerome Maes and Richard Mathot
Let us not forget our usability testing teams that tested it quite a lot of time.

Special thanks to Martin Trigaux that did nothing.
This commit is contained in:
Mantavya Gajjar
2015-01-16 16:01:59 +01:00
committed by Thibault Delavallée
parent cec43b93d2
commit 51be5bbfb3
79 changed files with 51293 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# ##############################################################################
#
# Odoo, Open Source Management Solution
# Copyright (C) 2014-TODAY Odoo SA (<https://www.odoo.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import controllers
import models
+34
View File
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
{
'name': 'Slides',
'version': '1.0',
'summary': 'Share and Publish Videos, Presentations and Documents',
'category': 'website',
'description': """
Share and Publish Videos, Presentations and Documents'
======================================================
* Website Application
* Channel Management
* Filters and Tagging
* Statistics of Presentation
* Channel Subscription
* Supported document types supported: PDF, images, YouTube videos and Google Drive documents)
""",
'author': 'Odoo SA',
'website': 'https://www.odoo.com',
'depends': ['website', 'website_mail'],
'data': [
'view/res_config.xml',
'view/website_slides.xml',
'view/website_slides_embed.xml',
'view/website_slides_backend.xml',
'data/website_slides_data.xml',
'security/ir.model.access.csv',
'security/website_slides_security.xml'
],
'demo': [
'data/website_slides_demo.xml'
],
'installable': True,
}
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import main
+349
View File
@@ -0,0 +1,349 @@
# -*- coding: utf-8 -*-
import base64
import logging
import werkzeug
from openerp.addons.web import http
from openerp.exceptions import AccessError
from openerp.http import request
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
class website_slides(http.Controller):
_slides_per_page = 12
_slides_per_list = 20
_order_by_criterion = {
'date': 'date_published desc',
'view': 'total_views desc',
'vote': 'likes desc',
}
def _set_viewed_slide(self, slide, view_mode):
slide_key = '%s_%s' % (view_mode, request.session_id)
viewed_slides = request.session.setdefault(slide_key, list())
if slide.id not in viewed_slides:
if view_mode == 'slide':
slide.sudo().slide_views += 1
elif view_mode == 'embed':
slide.sudo().embed_views += 1
viewed_slides.append(slide.id)
request.session[slide_key] = viewed_slides
return True
def _get_slide_detail(self, slide):
most_viewed_slides = slide.get_most_viewed_slides(self._slides_per_list)
related_slides = slide.get_related_slides(self._slides_per_list)
return {
'slide': slide,
'most_viewed_slides': most_viewed_slides,
'related_slides': related_slides,
'user': request.env.user,
'is_public_user': request.env.user == request.website.user_id,
'comments': slide.channel_id.can_see_full and slide.website_message_ids or [],
'private': not slide.channel_id.can_see_full,
}
# --------------------------------------------------
# MAIN / SEARCH
# --------------------------------------------------
@http.route('/slides', type='http', auth="public", website=True)
def slides_index(self, *args, **post):
""" Returns a list of available channels: if only one is available,
redirects directly to its slides
"""
channels = request.env['slide.channel'].search([], order='sequence, id')
if not channels:
return request.website.render("website_slides.channel_not_found")
elif len(channels) == 1:
return request.redirect("/slides/%s" % channels.id)
return request.website.render('website_slides.channels', {
'channels': channels,
'user': request.env.user,
'is_public_user': request.env.user == request.website.user_id
})
@http.route([
'/slides/<model("slide.channel"):channel>',
'/slides/<model("slide.channel"):channel>/page/<int:page>',
'/slides/<model("slide.channel"):channel>/<string:slide_type>',
'/slides/<model("slide.channel"):channel>/<string:slide_type>/page/<int:page>',
'/slides/<model("slide.channel"):channel>/tag/<model("slide.tag"):tag>',
'/slides/<model("slide.channel"):channel>/tag/<model("slide.tag"):tag>/page/<int:page>',
'/slides/<model("slide.channel"):channel>/category/<model("slide.category"):category>',
'/slides/<model("slide.channel"):channel>/category/<model("slide.category"):category>/page/<int:page>',
'/slides/<model("slide.channel"):channel>/category/<model("slide.category"):category>/<string:slide_type>',
'/slides/<model("slide.channel"):channel>/category/<model("slide.category"):category>/<string:slide_type>/page/<int:page>'],
type='http', auth="public", website=True)
def channel(self, channel, category=None, tag=None, page=1, slide_type=None, sorting='creation', search=None, **kw):
user = request.env.user
Slide = request.env['slide.slide']
domain = [('channel_id', '=', channel.id)]
pager_url = "/slides/%s" % (channel.id)
pager_args = {}
if search:
domain += [
'|', '|',
('name', 'ilike', search),
('description', 'ilike', search),
('index_content', 'ilike', search)]
pager_args['search'] = search
else:
if category:
domain += [('category_id', '=', category.id)]
pager_url += "/category/%s" % category.id
elif tag:
domain += [('tag_ids.id', '=', tag.id)]
pager_url += "/tag/%s" % tag.id
if slide_type:
domain += [('slide_type', '=', slide_type)]
pager_url += "/%s" % slide_type
if not sorting or sorting not in self._order_by_criterion:
sorting = 'date'
order = self._order_by_criterion[sorting]
pager_args['sorting'] = sorting
pager_count = Slide.search_count(domain)
pager = request.website.pager(url=pager_url, total=pager_count, page=page,
step=self._slides_per_page, scope=self._slides_per_page,
url_args=pager_args)
slides = Slide.search(domain, limit=self._slides_per_page, offset=pager['offset'], order=order)
values = {
'channel': channel,
'category': category,
'slides': slides,
'tag': tag,
'slide_type': slide_type,
'sorting': sorting,
'user': user,
'pager': pager,
'is_public_user': user == request.website.user_id,
}
if search:
values['search'] = search
return request.website.render('website_slides.slides_search', values)
# Display uncategorized slides
if not slide_type and not category:
category_datas = []
for category in Slide.read_group(domain, ['category_id'], ['category_id']):
category_id, name = category.get('category_id') or (False, _('Uncategorized'))
category_datas.append({
'id': category_id,
'name': name,
'total': category['category_id_count'],
'slides': Slide.search(category['__domain'], limit=4, offset=0, order=order)
})
values.update({
'category_datas': category_datas,
})
return request.website.render('website_slides.home', values)
# --------------------------------------------------
# SLIDE.SLIDE CONTOLLERS
# --------------------------------------------------
@http.route('/slides/slide/<model("slide.slide"):slide>', type='http', auth="public", website=True)
def slide_view(self, slide, **kwargs):
values = self._get_slide_detail(slide)
if not values.get('private'):
self._set_viewed_slide(slide, 'slide')
return request.website.render('website_slides.slide_detail_view', values)
@http.route('/slides/slide/<model("slide.slide"):slide>/pdf_content', type='http', auth="public", website=True)
def slide_get_pdf_content(self, slide):
response = werkzeug.wrappers.Response()
response.data = slide.datas.decode('base64')
response.mimetype = 'application/pdf'
return response
@http.route('/slides/slide/<model("slide.slide"):slide>/comment', type='http', auth="public", methods=['POST'], website=True)
def slide_comment(self, slide, **post):
""" Controller for message_post. Public user can post; their name and
email is used to find or create a partner and post as admin with the
right partner. Their comments are not published by default. Logged
users can post as usual. """
# TDE TODO :
# - fix _find_partner_from_emails -> is an api.one + strange results + should work as public user
# - subscribe partner instead of user writing the message ?
# - public user -> cannot create mail.message ?
if not post.get('comment'):
return werkzeug.utils.redirect(request.httprequest.referrer + "#discuss")
# public user: check or find author based on email, do not subscribe public user
# and do not publish their comments by default to avoid direct spam
if request.uid == request.website.user_id.id:
if not post.get('email'):
return werkzeug.utils.redirect(request.httprequest.referrer + "#discuss")
# TDE FIXME: public user has no right to create mail.message, should
# be investigated - using SUPERUSER_ID meanwhile
contextual_slide = slide.sudo().with_context(mail_create_nosubcribe=True)
# TDE FIXME: check in mail_thread, find partner from emails should maybe work as public user
partner_id = slide.sudo()._find_partner_from_emails([post.get('email')])[0][0]
if partner_id:
partner = request.env['res.partner'].sudo().browse(partner_id)
else:
partner = request.env['res.partner'].sudo().create({
'name': post.get('name', post['email']),
'email': post['email']
})
post_kwargs = {
'author_id': partner.id,
'website_published': False,
'email_from': partner.email,
}
# logged user: as usual, published by default
else:
contextual_slide = slide
post_kwargs = {}
contextual_slide.message_post(
body=post['comment'],
type='comment',
subtype='mt_comment',
**post_kwargs
)
return werkzeug.utils.redirect(request.httprequest.referrer + "#discuss")
@http.route('/slides/slide/<model("slide.slide"):slide>/download', type='http', auth="public", website=True)
def slide_download(self, slide):
if slide.download_security == 'public' or (slide.download_security == 'user' and request.session.uid):
filecontent = base64.b64decode(slide.datas)
disposition = 'attachment; filename=%s.pdf' % werkzeug.urls.url_quote(slide.name)
return request.make_response(
filecontent,
[('Content-Type', 'application/pdf'),
('Content-Length', len(filecontent)),
('Content-Disposition', disposition)])
elif not request.session.uid and slide.download_security == 'user':
return werkzeug.utils.redirect('/web?redirect=/slides/slide/%s' % (slide.id))
return request.website.render("website.403")
@http.route('/slides/slide/<model("slide.slide"):slide>/promote', type='http', auth='public', website=True)
def slide_set_promoted(self, slide):
slide.channel_id.promoted_slide_id = slide.id
return request.redirect("/slides/%s" % slide.channel_id.id)
# JSONRPC
@http.route('/slides/slide/like', type='json', auth="user", website=True)
def slide_like(self, slide_id):
slide = request.env['slide.slide'].browse(int(slide_id))
slide.likes += 1
return slide.likes
@http.route('/slides/slide/dislike', type='json', auth="user", website=True)
def slide_dislike(self, slide_id):
slide = request.env['slide.slide'].browse(int(slide_id))
slide.dislikes += 1
return slide.dislikes
@http.route(['/slides/slide/send_share_email'], type='json', auth='user', website=True)
def slide_send_share_email(self, slide_id, email):
slide = request.env['slide.slide'].browse(int(slide_id))
result = slide.send_share_email(email)
return result
@http.route('/slides/slide/overlay', type='json', auth="public", website=True)
def slide_get_next_slides(self, slide_id):
slide = request.env['slide.slide'].browse(int(slide_id))
slides_to_suggest = 9
def slide_mapped_dict(slide):
return {
'img_src': '/website/image/slide.slide/%s/image_thumb' % (slide.id),
'caption': slide.name,
'url': slide.share_url
}
vals = map(slide_mapped_dict, slide.get_related_slides(slides_to_suggest))
add_more_slide = slides_to_suggest - len(vals)
if max(add_more_slide, 0):
vals += map(slide_mapped_dict, slide.get_most_viewed_slides(add_more_slide))
return vals
# --------------------------------------------------
# TOOLS
# --------------------------------------------------
@http.route(['/slides/dialog_preview'], type='json', auth='user', methods=['POST'], website=True)
def dialog_preview(self, **data):
Slide = request.env['slide.slide']
document_type, document_id = Slide._find_document_data_from_url(data['url'])
preview = {}
if not document_id:
preview['error'] = _('Please enter valid youtube or google doc url')
return preview
existing_slide = Slide.search([('channel_id', '=', int(data['channel_id'])), ('document_id', '=', document_id)], limit=1)
if existing_slide:
preview['error'] = _('This video already exists in this channel <a target="_blank" href="/slides/slide/%s">click here to view it </a>' % existing_slide.id)
return preview
values = Slide._parse_document_url(data['url'], only_preview_fields=True)
if values.get('error'):
preview['error'] = _('Could not fetch data from url. Document or access right not available.\nHere is the received response: %s' % values['error'])
return preview
return values
@http.route(['/slides/add_slide'], type='json', auth='user', methods=['POST'], website=True)
def create_slide(self, *args, **post):
payload = request.httprequest.content_length
# payload is total request content size so it's not exact size of file.
# already add client validation this is for double check if client alter.
if (payload / 1024 / 1024 > 17):
return {'error': _('File is too big.')}
values = dict((fname, post[fname]) for fname in [
'name', 'url', 'tag_ids', 'slide_type', 'channel_id',
'mime_type', 'datas', 'description', 'image', 'index_content', 'website_published'] if post.get(fname))
if post.get('category_id'):
if post['category_id'][0] == 0:
values['category_id'] = request.env['slide.category'].create({
'name': post['category_id'][1]['name'],
'channel_id': values.get('channel_id')}).id
else:
values['category_id'] = post['category_id'][0]
# handle exception during creation of slide and sent error notification to the client
# otherwise client slide create dialog box continue processing even server fail to create a slide.
try:
slide_id = request.env['slide.slide'].create(values)
# TDE TODO: probably improve this behavior by differentiating exceptions
except Exception as e:
_logger.error(e)
return {'error': _('Internal server error, please try again later or contact administrator.\nHere is the error message: %s' % e.message)}
return {'url': "/slides/slide/%s" % (slide_id.id)}
# --------------------------------------------------
# EMBED IN THIRD PARTY WEBSITES
# --------------------------------------------------
@http.route('/slides/embed/<int:slide_id>', type='http', auth='public', website=True)
def slides_embed(self, slide_id, page="1", **kw):
# Note : don't use the 'model' in the route (use 'slide_id'), otherwise if public cannot access the embedded
# slide, the error will be the website.403 page instead of the one of the website_slides.embed_slide.
# Do not forget the rendering here will be displayed in the embedded iframe
# determine if it is embedded from external web page
referrer_url = request.httprequest.headers.get('Referer', '')
base_url = request.env['ir.config_parameter'].get_param('web.base.url')
is_embedded = referrer_url and not bool(base_url in referrer_url) or False
# try accessing slide, and display to corresponding template
try:
slide = request.env['slide.slide'].browse(slide_id)
if is_embedded:
request.env['slide.embed'].sudo().add_embed_url(slide.id, referrer_url)
values = self._get_slide_detail(slide)
values['page'] = page
values['is_embedded'] = is_embedded
if not values.get('private'):
self._set_viewed_slide(slide, 'embed')
return request.website.render('website_slides.embed_slide', values)
except AccessError: # TODO : please, make it clean one day, or find another secure way to detect
# if the slide can be embedded, and properly display the error message.
slide = request.env['slide.slide'].sudo().browse(slide_id)
return request.website.render('website_slides.embed_slide_forbidden', {'slide' : slide})
@@ -0,0 +1,161 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record model="ir.config_parameter" id="google_app_key">
<field name="key">website_slides.google_app_key</field>
<field name="value">AIzaSyDOWlmDW-7DbLmOR9ZsT5AOEXf4n6TFwQA</field>
<field name="group_ids" eval="[(4, ref('base.group_system'))]" />
</record>
<record id="website_menu_slides" model="website.menu">
<field name="name">Presentations</field>
<field name="url">/slides</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">50</field>
</record>
<record id="slide_template_published" model="mail.template">
<field name="name">Slide Published</field>
<field name="subject">New ${object.slide_type} published on ${object.channel_id.name}</field>
<field name="model_id" ref="model_slide_slide"/>
<field name="auto_delete" eval="True"/>
<field name="body_html"><![CDATA[
<div style="padding:0px; margin:0px;">
<table cellpadding="0" cellspacing="0" style="width: 600px; margin: 10px 0px 0px 0px; vertical-align: top; padding: 0px; font-family:arial; font-size:12px; background-color:#ffffff">
<tbody><tr>
<td style="width: 600px; text-align:center" valign="center">
<a href="${user.company_id.website}" style="text-decoration:none">
<img alt="Odoo" src="${ctx['base_url']}/logo.png" style="display:block; border:none; min-height:60px; margin:0 auto;">
</a>
</td>
</tr></tbody>
</table>
</div>
<div style="padding:0px; margin:0px;">
<table cellpadding="0" cellspacing="0" style="width: 600px; margin: 10px 0px 0px 0px; vertical-align: top; padding: 0px; font-family:arial; font-size:12px;">
<tbody><tr>
<td style="width: 600px; text-align:center; color: #ffffff; background-color:#a24689;" valign="center">
<h2 style="margin:0px;">
New ${object.slide_type} published on ${object.channel_id.name}
</h2>
</td>
</tr><tr>
<td style="width: 600px; vertical-align:top; text-align:center; background-color:#FFFFF; color:#414141">
<a href="${object.share_url}" style="color: #a24689;">
<img alt="${object.name}" src="${ctx['base_url']}/website/image/slide.slide/${object.id}/image" style="height:auto; width:500px; background-color: #cccccc; margin: 16px;">
</a>
<p style="font-size:24px; font-weight:bold"><a href="${object.share_url}">${object.name}</a></p>
<p style="color: #ccc;">Published on ${object.write_date}</p>
<p><a href="${object.share_url}">Click here to open the ${object.slide_type}</a></p>
</td>
</tr></tbody>
</table>
</div>
<div style="padding:0px; margin:0px;">
<table cellpadding="0" cellspacing="0" style="margin: 10px 0px 0px 0px; vertical-align: top;padding: 0px; font-family:arial; font-size:12px; color: #ffffff; background-color:#8f8f8f;">
<tbody><tr>
<td style="width: 600px; font-size:12px; text-align:center; padding-top:10px; color:#a24689; padding-bottom:5px">
<a href="${object.share_url}" style="color:#a24689">View in browser</a> |
<a href="/page/contactus" style="color:#a24689">Contact</a>
</td>
</tr></tbody>
</table>
</div>]]></field>
</record>
<record id="slide_template_shared" model="mail.template">
<field name="name">Slide Shared</field>
<field name="subject">${user.name} shared a ${object.slide_type} with you!</field>
<field name="model_id" ref="model_slide_slide"/>
<field name="email_from">${(user.email or '')|safe}</field>
<field name="email_to">${ctx['email']}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html"><![CDATA[
<div style="padding:0px; margin:0px;">
<table cellpadding="0" cellspacing="0" style="width: 600px; margin: 10px 0px 0px 0px; vertical-align: top; padding: 0px; font-family:arial; font-size:12px; background-color:#ffffff">
<tbody><tr>
<td style="width: 600px; text-align:center" valign="center">
<a href="${user.company_id.website}" style="text-decoration:none">
<img alt="Odoo" src="${ctx['base_url']}/logo.png" style="display:block; border:none; min-height:60px; margin:0 auto;">
</a>
</td>
</tr></tbody>
</table>
</div>
<div style="padding:0px; margin:0px;">
<table cellpadding="0" cellspacing="0" style="width: 600px; margin: 10px 0px 0px 0px; vertical-align: top; padding: 0px; font-family:arial; font-size:12px;">
<tbody><tr>
<td style="width: 600px; text-align:center; color: #ffffff; background-color:#a24689;" valign="center">
<h2 style="margin:0px;">
${user.name} shared the ${object.slide_type} ${object.name} with you!
</h2>
</td>
</tr><tr>
<td style="width: 600px; vertical-align:top; text-align:center; background-color:#FFFFF; color:#414141">
<a href="${object.share_url}" style="color: #a24689;">
<img alt="${object.name}" src="${ctx['base_url']}/website/image/slide.slide/${object.id}/image"
style="height:auto; width:500px; background-color: #cccccc; margin: 16px;">
</a>
<p style="font-size:24px; font-weight:bold"><a href="${object.share_url}">${object.name}</a></p>
<p style="color: #ccc;">Published on ${object.write_date}</p>
<p><a href="${object.share_url}">Click here to open the ${object.slide_type}</a></p>
</td>
</tr></tbody>
</table>
</div>
<div style="padding:0px; margin:0px;">
<table cellpadding="0" cellspacing="0" style="margin: 10px 0px 0px 0px; vertical-align: top;padding: 0px; font-family:arial; font-size:12px; color: #ffffff; background-color:#8f8f8f;">
<tbody><tr>
<td style="width: 600px; font-size:12px; text-align:center; padding-top:10px; color:#a24689; padding-bottom:5px">
<a href="${object.share_url}" style="color:#a24689">View in browser</a> |
<a href="/page/contactus" style="color:#a24689">Contact</a>
</td>
</tr></tbody>
</table>
</div>]]></field>
</record>
<record model="slide.channel" id="channel_public">
<field name="name">Public Channel</field>
<field name="promote_strategy">most_viewed</field>
<field name="website_published" eval="True"/>
<field name="visibility">public</field>
<field name="description">Default channel for slides, all public users can access content of this channel.</field>
</record>
<record model="slide.channel" id="channel_partial">
<field name="name">Partner Channel</field>
<field name="website_published" eval="True"/>
<field name="group_ids" eval="[(4, ref('base.group_portal')), (4, ref('base.group_user'))]"/>
<field name="upload_group_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="visibility">partial</field>
<field name="description">Used to publish slides in partner network privately.</field>
</record>
<record model="slide.channel" id="channel_private">
<field name="name">Employee Channel</field>
<field name="group_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="visibility">private</field>
<field name="description">Used to publish internal slides of company.</field>
</record>
<!-- Channel subtypes -->
<record id="mt_channel_slide_published" model="mail.message.subtype">
<field name="name">Presentation Published</field>
<field name="res_model">slide.channel</field>
<field name="default" eval="True"/>
<field name="description">Presentation Published</field>
</record>
<!-- jump to Channel on installation complete -->
<record id="action_open_channels" model="ir.actions.act_url">
<field name="name">Channels</field>
<field name="target">self</field>
<field name="url" eval="'/slides'"/>
</record>
<record id="base.open_menu_channels" model="ir.actions.todo">
<field name="action_id" ref="action_open_channels"/>
<field name="type">automatic</field>
<field name="state">open</field>
</record>
</data>
</openerp>
@@ -0,0 +1,248 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="category_1" model="slide.category">
<field name="name">Featured Presentations</field>
<field name="channel_id" ref="channel_public"/>
<field name="sequence">10</field>
</record>
<record id="category_2" model="slide.category">
<field name="name">Odoo Days</field>
<field name="channel_id" ref="channel_public"/>
<field name="sequence">20</field>
</record>
<record id="tag1" model="slide.tag">
<field name="name">CMS</field>
</record>
<record id="tag2" model="slide.tag">
<field name="name">Marketing</field>
</record>
<record id="tag3" model="slide.tag">
<field name="name">POS</field>
</record>
<record id="tag4" model="slide.tag">
<field name="name">Customer Voice</field>
</record>
<record id="tag5" model="slide.tag">
<field name="name">Odoo Days</field>
</record>
<record id="slide_0" model="slide.slide">
<field name="name">Odoo Explained</field>
<field name="url">http://www.youtube.com/watch?v=o0UM3Lg7K4Y</field>
<field name="channel_id" ref="channel_public"/>
<field name="category_id" ref="category_1"/>
<field name="website_published" eval="True"/>
<field name="tag_ids" eval="[(4, ref('tag2'))]"/>
</record>
<record id="slide_1" model="slide.slide">
<field name="name">Open Source Website Builder and eCommerce: OpenERP</field>
<field name="url">https://www.youtube.com/watch?v=pp7OCqWoRjQ</field>
<field name="channel_id" ref="channel_public"/>
<field name="category_id" ref="category_1"/>
<field name="website_published" eval="True"/>
<field name="tag_ids" eval="[(4, ref('tag1'))]"/>
</record>
<record id="slide_2" model="slide.slide">
<field name="name">Odoo Open Days 2014</field>
<field name="url">https://www.youtube.com/watch?v=ILTPZUTWKhg&amp;list=UUkQPikELWZFLgQNHd73jkdg</field>
<field name="channel_id" ref="channel_public"/>
<field name="category_id" ref="category_2"/>
<field name="website_published" eval="True"/>
<field name="tag_ids" eval="[(4, ref('tag3'))]"/>
</record>
<record id="slide_3" model="slide.slide">
<field name="name">Odoo - The Voice of the Customer: Alpinter</field>
<field name="url">https://www.youtube.com/watch?v=5DPGnN9_uKU&amp;list=UUkQPikELWZFLgQNHd73jkdg</field>
<field name="channel_id" ref="channel_public"/>
<field name="category_id" ref="category_2"/>
<field name="website_published" eval="True"/>
<field name="tag_ids" eval="[(4, ref('tag4'))]"/>
</record>
<record id="slide_4" model="slide.slide">
<field name="name">OpenERP SEO - Boost Your Website Trafic</field>
<field name="url">https://www.youtube.com/watch?v=5PKqZpl0TcA</field>
<field name="channel_id" ref="channel_public"/>
<field name="category_id" ref="category_1"/>
<field name="website_published" eval="True"/>
<field name="tag_ids" eval="[(4, ref('tag1'))]"/>
</record>
<record id="slide_5" model="slide.slide">
<field name="name">OpenERP POS Indiegogo Campaign</field>
<field name="url">https://www.youtube.com/watch?v=C_Laaz4mORg</field>
<field name="channel_id" ref="channel_public"/>
<field name="category_id" ref="category_1"/>
<field name="website_published" eval="True"/>
<field name="tag_ids" eval="[(4, ref('tag3'))]"/>
</record>
<record id="slide_6" model="slide.slide">
<field name="name">Odoo Marketing - part 1: ATTRACT</field>
<field name="url">https://www.youtube.com/watch?v=qhSCMShJkTY&amp;list=UUkQPikELWZFLgQNHd73jkdg</field>
<field name="channel_id" ref="channel_public"/>
<field name="category_id" ref="category_1"/>
<field name="website_published" eval="True"/>
<field name="tag_ids" eval="[(4, ref('tag1')), (4, ref('tag2'))]"/>
</record>
<record id="slide_7" model="slide.slide">
<field name="name">Odoo Marketing - part 1: ATTRACT_2</field>
<field name="url">https://www.youtube.com/watch?v=L1Ye6ATzZXY&amp;list=UUkQPikELWZFLgQNHd73jkdg</field>
<field name="channel_id" ref="channel_public"/>
<field name="category_id" ref="category_1"/>
<field name="website_published" eval="True"/>
<field name="tag_ids" eval="[(4, ref('tag1')), (4, ref('tag2'))]"/>
</record>
<record id="slide_8" model="slide.slide">
<field name="name">Odoo Marketing - part 1: ATTRACT_3</field>
<field name="url">https://www.youtube.com/watch?v=sEvZyNZCtOs&amp;list=UUkQPikELWZFLgQNHd73jkdg</field>
<field name="channel_id" ref="channel_public"/>
<field name="category_id" ref="category_1"/>
<field name="website_published" eval="True"/>
<field name="tag_ids" eval="[(4, ref('tag1')), (4, ref('tag2'))]"/>
</record>
<record id="slide_9" model="slide.slide">
<field name="name">Open Days 2014 Infographic</field>
<field name="datas" type="base64" file="website_slides/static/src/img/Open Days 2014 |Infographic.png"/>
<field name="image" type="base64" file="website_slides/static/src/img/Open Days 2014 |Infographic.png"/>
<field name="slide_type">infographic</field>
<field name="channel_id" ref="channel_public"/>
<field name="category_id" ref="category_2"/>
<field name="website_published" eval="True"/>
<field name="likes">13</field>
<field name="slide_views">7399</field>
<field name="tag_ids" eval="[(4, ref('tag5'))]"/>
</record>
<record id="slide_10" model="slide.slide">
<field name="name">Odoo Roadmap &amp; Strategy - Fabien Pinckaers</field>
<field name="datas" type="base64" file="website_slides/static/src/img/keynote.pdf"/>
<field name="image" type="base64" file="website_slides/static/src/img/keynote.png"/>
<field name="slide_type">presentation</field>
<field name="channel_id" ref="channel_partial"/>
<field name="category_id" ref="category_2"/>
<field name="website_published" eval="True"/>
<field name="likes">13</field>
<field name="slide_views">6399</field>
<field name="index_content">1. Odoo Roadmap &amp; Strategy Fabien Pinckaers, Founder
2. From 2,000,000 users to 10,000,000 users. or how we will turn Odoo mainstream...
3. from $2M to $10M Investment Areas To Turn Odoo Mainstream 5 or how to spend $10M as fast as possible and burn a lot of energy...
4. Release A Product That Deprecates Every Competitor 1 This is something we excel at...
5. A product growth no one can follow
6. Version 9: roadmap o 40% Usability: – Improving all existing apps, the right feature at the right place – User onboarding, ease of use o 20% Accounting: – 8 countries in the core o 20% CMS / eCommerce o 10% New design: – Merge front/backend, mobile, modern design o 10% Framework: o New API
7. 1 Release cycle February 2012: v6.1 (13 months) 2 January 2013: v7 (11 months) 3 June 2014: v8 (18 months) 4 June 2015: v9 (12 months) 5 Every new version is a revolution. Version 9 will be even more impressive! January 2011: v6.0
8. Attract The Mass Of Users 2 It's all about getting users, a lot of users. Not only customers.
9. An attractive pricing... o The cloud, free for 2 users: – Why the cloud? Smooth user experience. High satisfaction rates as everything works out-of-the-box – Unlimited applications: CRM, CMS, eCommerce, Accounting, etc. o Millions of CMS/eCommerce for free: – Already 50k pages on CMS/eCommerce websites built with Odoo CMS Beta – You can do a lot with 2 users: we don't need to get revenues from CMS/eCommerce-only instances as we have so many applications to upsell
10. Affordable implementations o Lower entry barrier - no huge starting costs for SMEs: – No complex implementation projects – A fixed-price packaged offer o New service offer, 100€ / month per application for: – Implementation assistance and configuration – Unlimited customization of screens, reports, workflows – Monthly upgrades – Training material – Unlimited support tickets, 24/24 response time!
11. Disrupt A Market Having A Huge Traction 3 Creating the frontend of all apps will not only increase the quality of all apps but also disrupt the CMS/eCommerce market.
12. " " The CMS/eCommerce market looks like the ERP market 6 years ago, before Odoo entered into it. Full of complex products that did not evolve over the past 3 years.
13. Traditional ERPs o The monetization (and main focus) remains on business apps: – Accounting – Warehouse Management – Project Management – Point of sale – Marketing – HR, ... o But CMS/eCommerce will bring a huge volume of prospects to our other business apps
14. Make Partners Rich By Making Customers Super-Happy 4 You will like our product and services so much that you will always want to purchase more.
15. Traditional ERPs o Buy an ERP – Invest on 7 years o Maintain as long as you can – Implementations are so painful that you have no other choice – Upgrades are too expensive o After 7 years : it breaks – You change the product and call other vendors
16. Traditional ERPs o Modern companies deserve to: – Evolve continuously – Benefit from top features every year – Be free of any legacy constraints – Have the flexibility to customize easily o Odoo Enterprise – our main value proposition: – Providing so much value to customers through continuous upgrades that they will want to purchase even more
17. Odoo Enterprise o Odoo Enterprise is our core business: – Unlimited upgrades – Unlimited bugfix support – Optional hosting on our cloud – Guarantees : security, performance, legal o Customers shouldn't have to pay for: – Painful upgrades; if upgrades are costly, they tend to not upgrade – Bugs take delay the project and costs man*days of work
18. " " Story of a 1300 users customer I met last week...
19. Let's Burn A Few Millions In Marketing Too 5 We have an awesome product, a huge channel of partners, a strong service offer, ... Now, people need to know it.
20.
21. Marketing activities o Smart Content: – Documentation, books, blog posts, white papers, Q&amp;A – Translations of website and documentation – A super clean website, marketing optimized ! o Inbound Marketing: – SEO, SEA, funnel optimization with a powerful CMS o User/developer on boarding: – Users : usability, clean features, etc. – Devs: docs, easy installation, modules scafolding, ... – Community: efficient tools and communication o Local Events: share, meet and inform people
22. All of this while maintaining a fun working environment and a sane relationship between partners, customers and the community.
23. Relationship between proprietary software vendors and their implementation partners
24. What we do By enforcing full open source rules, we set up the rules to guarantee a sane relationship between all parties of the value chain: ● Vendor: Odoo SA ● Customers ● Partners ● Community
25. 2014-2015 This will be an epic time...</field>
<field name="tag_ids" eval="[(4, ref('tag5'))]"/>
</record>
<record id="slide_11" model="slide.slide">
<field name="name">OpenSource CMS, A performance comparison - Mantavya Gajjar</field>
<field name="datas" type="base64" file="website_slides/static/src/img/odoo-cms-comparision.pdf"/>
<field name="image" type="base64" file="website_slides/static/src/img/odoo-cms-comparision.png"/>
<field name="slide_type">presentation</field>
<field name="channel_id" ref="channel_public"/>
<field name="category_id" ref="category_2"/>
<field name="website_published" eval="True"/>
<field name="likes">10</field>
<field name="slide_views">4718</field>
<field name="index_content">1. Open Source CMS A performance comparision Mantavya Gajjar – Managining Director, OpenERP India
2. Topics 1 2 3 4 5 o Introduction – Products to Compare – Test Environment o Test Cases (home, catalogue, product pages) – Performance ● Google PageSpeed Inside ● Speed Test using Pingdom – Scalability ● Load Test using Blitz – Maintainability ● Source Code – cloc ● Community to maintain the source code – W3C Markup Validations o Conclusion
3. Introduction
4. Introduction Open Source Products, compared with Odoo
5. Odoo o Version Saas4 o Standard Data / Modules – Demo data with Odoo Website, Commerce, Forum, News, etc
6. Magento o Version 1.8.1.0 o Custom Data / Module – Home page and widgets – Added some products and their description
7. Drupal o Version 7.27 o Custom Data / Modules – CK-Editor &amp; CK-Finder – Ubercart for e-commerce, Rules, ctools, views, entity
8. Prestashop o Version 1.6 o No Custom Data / Modules – Use default demo shop
9. Performance Google PageSpeed Insight &amp; Pingdom
10. Google PageSpeed Insights Home page on desktop Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/
11. Home Page
12. Odoo vs Drupal Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
13. Odoo vs Magento Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
14. Odoo vs Prestashop Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
15. Summary : Google Page Speed Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
16. Google Page Speed Insights Home page on mobile Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/
17. Odoo Vs Drupal Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
18. Odoo Vs Magento Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
19. Odoo Vs Prestashop Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
20. Summary : Google Page Speed Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
21. Google PageSpeed Insights Product Catalogue on Desktop Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/
22. Odoo Vs Drupal Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
23. Odoo Vs Magento Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
24. Odoo Vs Prestashop Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
25. Summary : Google Page Speed Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
26. Google PageSpeed Insights Product Catalogue on Mobile Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/
27. Odoo Vs Drupal Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
28. Odoo Vs Magento Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
29. Odoo Vs Prestashop Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
30. Summary : Google Page Speed Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
31. Google PageSpeed Insights Product Page on Desktop Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/
32. Odoo Vs Drupal Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
33. Odoo Vs Magento Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
34. Odoo Vs Prestashop Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
35. Summary : Google Page Speed Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
36. Google PageSpeed Insights Product Page on Mobile Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/
37. Odoo Vs Drupal Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
38. Odoo Vs Magento Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
39. Odoo Vs Preshashop Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
40. Summary : Google Page Speed Computed by Google Page Speed Insight : https://developers.google.com/speed/pagespeed/insights/ Rating is out of 100
41. Speed Test using Pingdom Resources Loads to render page
42. Resources Load to render page As browser can load only 4 fles in parallal on midsie traffc website, then latency time and number of resources to donwload impact time to load more resources Odoo load faster because concatenate several fles, minify and gzip the content, even all the modules installed
43. Resources Load to render page
44. Resources Load to render page
45. Scaleablity Using Blitz, simulate the 250 users
46. Load Test using Blitz Region – Virginia | Duration – 60 Seconds | Load – 1 To 250 Users Computed using Blitz: https://www.blitz.io
47. Software Confgurations Magento, Prestashop, Drupa o Ubuntu 14.04 latest LTS o Apache 2.2 Server o PHP 5 Framework o MySQL Server 5.0 Odoo o Ubuntu 14.04 latest LTS o Nginx 1.4 o Odoo Framework o PostgreSQL 9.1
48. Hardware Confguration o IBM Server Series X3500 M4, 15 MB L3 Cache o 300 GB GSAS Drive 10K RPMs o Intel(R) Xeon(R) CPU E5-2620 0 @ 2.00GHz, 12 cores o 8 GB Physical RAM o 15 MBPS Dedicated Internet connection line Network : o 15 MBPS dedicated Internet connection
49. Odoo Region – Virginia | Duration – 60 Seconds | Load – 1 To 250 Users The max hit rate was: 88 hits per second Website and Ecommerce apps installed Confguration :
50. Odoo ● Fastest: 530 ms | Slowest: 1655 ms | Average: 829 ms ● First time out happened at 47.4 second at 198 users ● 3646 successful hits in 60 sec, avg 52,45,920 hits/day capacity. Response Time :
51. Drupal Region – Virginia | Duration – 60 Seconds | Load – 1 To 250 Users The max hit rate was : 37 hits per second Confguration :
52. Drupal ● Fastest: 1172 ms | Slowest: 4943 ms | Average: 2141 ms ● First time out happened at 30 second at 125 users ● 1,083 successful hits in 60 sec, avg 15,59,520 hits/day capacity. Response Time :
53. Magento Region – Virginia | Duration – 60 Seconds | Load – 1 To 250 Users The max hit rate was : 21 hits per second Confguration :
54. Magento ● Fastest: 1639 ms | Slowest: 16315 ms | Average: 7813 ms ● First time out happened at 35 second at 146 users ● 528 successful hits in 60 sec, avg 0.7 M hits/day capacity. Response Time :
55. Prestashop Region – Virginia | Duration – 60 Seconds | Load – 1 To 250 Users The max hit rate was : 16 hits per second Confguration :
56. Prestashop ● Fastest: 1678 ms | Slowest: 9228 ms | Average: 5598 ms ● First time out happened at 25 second at 104 users ● 663 successful hits in 60 sec, avg 0.95 M hits/day capacity. Response Time :
57. Load Test Summary
58. Summary Speed : o Odoo is 3x times faster than the Drupal o Odoo is 6x times faster than the Magento o Odoo is 2x times faster than the Prestashop Scalability : o Odoo timeout at 198 users o Drupal timeout at 125 users o Magento timeout at 148 users o Prestashop timeout at 104 users
59. W3C Markup Validation HTML Validation
60. W3C Check Odoo's main problem o Element meta is missing one or more of the following attributes: content, property. o An img element must have an alt attribute, except under certain conditions o The width attribute on the table element is obsolete. Use CSS instead. Team is working on those issues , will be fxed soon
61. Maintainability lines of code and contributors
62. Test 1 : Source Code Comparision between : o Odoo : includes all modules. I.e accounting, wms, mrp, websites, etc... o Drupal, Magento and Prestashop : only core modules Odoo is : o 1.3x times smaller then the drupal o 5.5x times smaller then the magento o 1.9x times smaller then the prestashop
63. Odoo Statistic provided by ohloh.net available at https://www.ohloh.net/p/odoo
64. Drupal Statistic provided by ohloh.net available at https://www.ohloh.net/p/drupal
65. Magento Statistic provided by ohloh.net available at https://www.ohloh.net/p/magento
66. Prestashop Statistic provided by ohloh.net available at https://www.ohloh.net/p/PrestaShop
67. Conclusion
68. Conclusion Performance : o Odoo is 3x times faster than the Drupal o Odoo is 6x times faster than the Magento o Odoo is 2x times faster than the Prestashop Scalability : o Odoo timeout at 198 users o Drupal timeout at 125 users o Magento timeout at 148 users o Prestashop timeout at 104 users Maintainability : o 1.3x times smaller then the drupal o 5.5x times smaller then the magento o 1.9x times smaller then the prestashop
69. Questions ?
70. Odoo sales@odoo.com +32 (0) 2 290 34 90 www.odoo.com R&amp;D and services offce Chaussée de Namur 40 B-1367 Grand Rosière Sales offce Avenue Van Nieuwenhuyse 5 B-1160 Brussels Thank You</field>
</record>
<record model="slide.channel" id="channel_public">
<field name="promote_strategy">custom</field>
<field name="custom_slide_id" ref="slide_0"/>
</record>
</data>
</openerp>
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import res_config
import slides
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from openerp import api, fields, models
class website_config_settings(models.TransientModel):
_inherit = "website.config.settings"
website_slide_google_app_key = fields.Char(string='Google Doc Key')
@api.model
def get_website_slide_google_app_key(self, fields):
website_slide_google_app_key = False
if 'website_slide_google_app_key' in fields:
website_slide_google_app_key = self.env['ir.config_parameter'].sudo().get_param('website_slides.google_app_key')
return {
'website_slide_google_app_key': website_slide_google_app_key
}
@api.multi
def set_website_slide_google_app_key(self):
for wizard in self:
self.env['ir.config_parameter'].sudo().set_param('website_slides.google_app_key', wizard.website_slide_google_app_key)
+509
View File
@@ -0,0 +1,509 @@
# -*- coding: utf-8 -*-
import datetime
import io
import json
from PIL import Image
import re
from urllib import urlencode
import urllib2
from urlparse import urlparse
from openerp import api, fields, models, SUPERUSER_ID, _
from openerp.tools import image
from openerp.exceptions import Warning
from openerp.addons.website.models.website import slug
class Channel(models.Model):
""" A channel is a container of slides. It has group-based access configuration
allowing to configure slide upload and access. Slides can be promoted in
channels. """
_name = 'slide.channel'
_description = 'Channel for Slides'
_inherit = ['mail.thread', 'website.seo.metadata']
_order = 'sequence, id'
_order_by_strategy = {
'most_viewed': 'total_views desc',
'most_voted': 'likes desc',
'latest': 'date_published desc',
}
name = fields.Char('Name', translate=True, required=True)
website_published = fields.Boolean('Published', help="Published on the website", copy=False)
description = fields.Html('Description', translate=True)
sequence = fields.Integer(default=10, help='Display order')
category_ids = fields.One2many('slide.category', 'channel_id', string="Categories")
slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides")
promote_strategy = fields.Selection([
('none', 'No Featured Presentation'),
('latest', 'Latest Published'),
('most_voted', 'Most Voted'),
('most_viewed', 'Most Viewed'),
('custom', 'Featured Presentation')],
string="Featuring Policy", default='most_voted', required=True)
custom_slide_id = fields.Many2one('slide.slide', string='Slide to Promote')
promoted_slide_id = fields.Many2one('slide.slide', string='Featured Slide', compute='_compute_promoted_slide_id', store=True)
@api.depends('custom_slide_id', 'promote_strategy', 'slide_ids.likes',
'slide_ids.total_views', "slide_ids.date_published")
def _compute_promoted_slide_id(self):
for record in self:
if record.promote_strategy == 'none':
record.promoted_slide_id = False
elif record.promote_strategy == 'custom':
record.promoted_slide_id = record.custom_slide_id
elif record.promote_strategy:
slides = self.env['slide.slide'].search(
[('website_published', '=', True), ('channel_id', '=', record.id)],
limit=1, order=self._order_by_strategy[record.promote_strategy])
record.promoted_slide_id = slides and slides[0] or False
nbr_presentations = fields.Integer('Number of Presentations', compute='_count_presentations', store=True)
nbr_documents = fields.Integer('Number of Documents', compute='_count_presentations', store=True)
nbr_videos = fields.Integer('Number of Videos', compute='_count_presentations', store=True)
nbr_infographics = fields.Integer('Number of Infographics', compute='_count_presentations', store=True)
total = fields.Integer(compute='_count_presentations', store=True)
@api.depends('slide_ids.slide_type', 'slide_ids.website_published')
def _count_presentations(self):
result = dict.fromkeys(self.ids, dict())
res = self.env['slide.slide'].read_group(
[('website_published', '=', True), ('channel_id', 'in', self.ids)],
['channel_id', 'slide_type'], ['channel_id', 'slide_type'],
lazy=False)
for res_group in res:
result[res_group['channel_id'][0]][res_group['slide_type']] = result[res_group['channel_id'][0]].get(res_group['slide_type'], 0) + res_group['__count']
for record in self:
record.nbr_presentations = result[record.id].get('presentation', 0)
record.nbr_documents = result[record.id].get('document', 0)
record.nbr_videos = result[record.id].get('video', 0)
record.nbr_infographics = result[record.id].get('infographic', 0)
record.total = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics
publish_template_id = fields.Many2one(
'mail.template', string='Published Template',
help="Email template to send slide publication through email",
default=lambda self: self.env['ir.model.data'].xmlid_to_res_id('website_slides.slide_template_published'))
share_template_id = fields.Many2one(
'mail.template', string='Shared Template',
help="Email template used when sharing a slide",
default=lambda self: self.env['ir.model.data'].xmlid_to_res_id('website_slides.slide_template_shared'))
visibility = fields.Selection([
('public', 'Public'),
('private', 'Private'),
('partial', 'Show channel but restrict presentations')],
default='public', required=True)
group_ids = fields.Many2many(
'res.groups', 'rel_channel_groups', 'channel_id', 'group_id',
string='Channel Groups', help="Groups allowed to see presentations in this channel")
access_error_msg = fields.Html(
'Error Message', help="Message to display when not accessible due to access rights",
default="<p>This channel is private and its content is restricted to some users.</p>", translate=True)
upload_group_ids = fields.Many2many(
'res.groups', 'rel_upload_groups', 'channel_id', 'group_id',
string='Upload Groups', help="Groups allowed to upload presentations in this channel. If void, every user can upload.")
# not stored access fields, depending on each user
can_see = fields.Boolean('Can See', compute='_compute_access')
can_see_full = fields.Boolean('Full Access', compute='_compute_access')
can_upload = fields.Boolean('Can Upload', compute='_compute_access')
@api.one
@api.depends('visibility', 'group_ids', 'upload_group_ids')
def _compute_access(self):
self.can_see = self.visibility in ['public', 'private'] or bool(self.group_ids & self.env.user.groups_id)
self.can_see_full = self.visibility == 'public' or bool(self.group_ids & self.env.user.groups_id)
self.can_upload = self.can_see and (not self.upload_group_ids or bool(self.upload_group_ids & self.env.user.groups_id))
@api.onchange('visibility')
def change_visibility(self):
if self.visibility == 'public':
self.group_ids = False
class Category(models.Model):
""" Channel contain various categories to manage its slides """
_name = 'slide.category'
_description = "Slides Category"
_order = "sequence, id"
name = fields.Char('Name', translate=True, required=True)
channel_id = fields.Many2one('slide.channel', string="Channel", required=True)
sequence = fields.Integer(default=10, help='Display order')
slide_ids = fields.One2many('slide.slide', 'category_id', string="Slides")
nbr_presentations = fields.Integer("Number of Presentations", compute='_count_presentations', store=True)
nbr_documents = fields.Integer("Number of Documents", compute='_count_presentations', store=True)
nbr_videos = fields.Integer("Number of Videos", compute='_count_presentations', store=True)
nbr_infographics = fields.Integer("Number of Infographics", compute='_count_presentations', store=True)
total = fields.Integer(compute='_count_presentations', store=True)
@api.depends('slide_ids.slide_type', 'slide_ids.website_published')
def _count_presentations(self):
result = dict.fromkeys(self.ids, dict())
res = self.env['slide.slide'].read_group(
[('website_published', '=', True), ('category_id', 'in', self.ids)],
['category_id', 'slide_type'], ['category_id', 'slide_type'],
lazy=False)
for res_group in res:
result[res_group['category_id'][0]][res_group['slide_type']] = result[res_group['category_id'][0]].get(res_group['slide_type'], 0) + res_group['__count']
for record in self:
record.nbr_presentations = result[record.id].get('presentation', 0)
record.nbr_documents = result[record.id].get('document', 0)
record.nbr_videos = result[record.id].get('video', 0)
record.nbr_infographics = result[record.id].get('infographic', 0)
record.total = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics
class EmbeddedSlide(models.Model):
""" Embedding in third party websites. Track view count, generate statistics. """
_name = 'slide.embed'
_description = 'Embedded Slides View Counter'
_rec_name = 'slide_id'
slide_id = fields.Many2one('slide.slide', string="Presentation", required=True, select=1)
url = fields.Char('Third Party Website URL', required=True)
count_views = fields.Integer('# Views', default=1)
def add_embed_url(self, slide_id, url):
schema = urlparse(url)
baseurl = schema.netloc
embeds = self.search([('url', '=', baseurl), ('slide_id', '=', int(slide_id))], limit=1)
if embeds:
embeds.count_views += 1
else:
embeds = self.create({
'slide_id': slide_id,
'url': baseurl,
})
return embeds.count_views
class SlideTag(models.Model):
""" Tag to search slides accross channels. """
_name = 'slide.tag'
_description = 'Slide Tag'
name = fields.Char('Name', required=True)
_sql_constraints = [
('slide_tag_unique', 'UNIQUE(name)', 'A tag must be unique!'),
]
class Slide(models.Model):
""" This model represents actual presentations. Those must be one of four
types:
- Presentation
- Document
- Infographic
- Video
Slide has various statistics like view count, embed count, like, dislikes """
_name = 'slide.slide'
_inherit = ['mail.thread', 'website.seo.metadata']
_description = 'Slides'
_PROMOTIONAL_FIELDS = [
'__last_update', 'name', 'image_thumb', 'slide_type', 'total_views', 'category_id',
'channel_id', 'description', 'tag_ids', 'write_date', 'create_date',
'website_published', 'website_meta_title', 'website_meta_description', 'website_meta_keywords']
_sql_constraints = [
('name_uniq', 'UNIQUE(channel_id, name)', 'The slide name must be unique within a channel')
]
# description
name = fields.Char('Title', required=True, translate=True)
description = fields.Text('Description', translate=True)
channel_id = fields.Many2one('slide.channel', string="Channel", required=True)
category_id = fields.Many2one('slide.category', string="Category", domain="[('channel_id', '=', channel_id)]")
tag_ids = fields.Many2many('slide.tag', 'rel_slide_tag', 'slide_id', 'tag_id', string='Tags')
download_security = fields.Selection(
[('none', 'No One'), ('user', 'Authentified Users Only'), ('public', 'Everyone')],
string='Download Security',
required=True, default='user')
image = fields.Binary('Image')
image_medium = fields.Binary('Medium', compute="_get_image", store=True)
image_thumb = fields.Binary('Thumbnail', compute="_get_image", store=True)
@api.depends('image')
def _get_image(self):
for record in self:
if record.image:
record.image_medium = image.crop_image(record.image, thumbnail_ratio=3)
record.image_thumb = image.crop_image(record.image, thumbnail_ratio=4)
else:
record.image_medium = False
record.iamge_thumb = False
# content
slide_type = fields.Selection([
('infographic', 'Infographic'),
('presentation', 'Presentation'),
('document', 'Document'),
('video', 'Video')],
string='Type', required=True,
default='document',
help="Document type will be set automatically depending on file type, height and width.")
index_content = fields.Text('Transcript')
datas = fields.Binary('Content')
url = fields.Char('Document URL', help="Youtube or Google Document URL")
document_id = fields.Char('Document ID', help="Youtube or Google Document ID")
mime_type = fields.Char('Mime-type')
@api.onchange('url')
def on_change_url(self):
self.ensure_one()
if self.url:
res = self._parse_document_url(self.url)
if res.get('error'):
raise Warning(_('Could not fetch data from url. Document or access right not available:\n%s') % res['error'])
values = res['values']
if not values.get('document_id'):
raise Warning(_('Please enter valid Youtube or Google Doc URL'))
for key, value in values.iteritems():
setattr(self, key, value)
# website
website_published = fields.Boolean('Published', help="Published on the website", copy=False)
date_published = fields.Datetime('Publish Date')
website_message_ids = fields.One2many(
'mail.message', 'res_id',
domain=lambda self: [('model', '=', self._name), ('type', '=', 'comment')],
string='Website Messages', help="Website communication history")
likes = fields.Integer('Likes')
dislikes = fields.Integer('Dislikes')
# views
embedcount_ids = fields.One2many('slide.embed', 'slide_id', string="Embed Count")
slide_views = fields.Integer('# of Website Views')
embed_views = fields.Integer('# of Embedded Views')
total_views = fields.Integer("Total # Views", default="0", compute='_compute_total', store=True)
@api.depends('slide_views', 'embed_views')
def _compute_total(self):
for record in self:
record.total_views = record.slide_views + record.embed_views
share_url = fields.Char('Share URL', readonly=True, compute='_get_share_url')
def _get_share_url(self):
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
for record in self:
record.share_url = "%s/slides/slide/%s" % (base_url, slug(record))
embed_code = fields.Text('Embed Code', readonly=True, compute='_get_embed_code')
def _get_embed_code(self):
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
for record in self:
if record.datas and not record.document_id:
record.embed_code = '<iframe src="%s/slides/embed/%s?page=1" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % (base_url, record.id, 315, 420)
elif record.slide_type == 'video' and record.document_id:
if not record.mime_type:
# embed youtube video
record.embed_code = '<iframe src="//www.youtube.com/embed/%s?theme=light" frameborder="0"></iframe>' % (record.document_id)
else:
# embed google doc video
record.embed_code = '<embed src="https://video.google.com/get_player?ps=docs&partnerid=30&docid=%s" type="application/x-shockwave-flash"></embed>' % (record.document_id)
else:
record.embed_code = False
@api.model
def create(self, values):
if not values.get('index_content'):
values['index_content'] = values.get('description')
if values.get('slide_type') == 'infographic' and not values.get('image'):
values['image'] = values['datas']
if values.get('website_published') and not values.get('date_published'):
values['date_published'] = datetime.datetime.now()
if values.get('url'):
doc_data = self._parse_document_url(values['url']).get('values', dict())
for key, value in doc_data.iteritems():
values.setdefault(key, value)
# Do not publish slide if user has not publisher rights
if not self.user_has_groups('base.group_website_publisher'):
values['website_published'] = False
slide = super(Slide, self).create(values)
slide.channel_id.message_subscribe_users()
slide._post_publication()
return slide
@api.multi
def write(self, values):
if values.get('url'):
doc_data = self._parse_document_url(values['url']).get('values', dict())
for key, value in doc_data.iteritems():
values.setdefault(key, value)
res = super(Slide, self).write(values)
if values.get('website_published'):
self.date_published = datetime.datetime.now()
self._post_publication()
return res
@api.model
def check_field_access_rights(self, operation, fields):
""" As per channel access configuration (visibility)
- public ==> no restriction on slides access
- private ==> restrict all slides of channel base on access group defined on channel group_ids field
- partial ==> show channel, but presentations based on groups means any user can see channel but not slide's content.
For private: implement using record rule
For partial: user can see channel, but channel gridview have slide detail so we have to implement
partial field access mechanism for public user so he can have access of promotional field (name, view_count) of slides,
but not all fields like data (actual pdf content)
all fields should be accessible only for user group define on channel group_ids
"""
if self.env.uid == SUPERUSER_ID:
return fields or list(self._fields)
fields = super(Slide, self).check_field_access_rights(operation, fields)
# still read not perform so we can not access self.channel_id
if self.ids:
self.env.cr.execute('SELECT DISTINCT channel_id FROM ' + self._table + ' WHERE id IN %s', (tuple(self.ids),))
channel_ids = [x[0] for x in self.env.cr.fetchall()]
channels = self.env['slide.channel'].sudo().browse(channel_ids)
limited_access = all(channel.visibility == 'partial' and
not len(channel.group_ids & self.env.user.groups_id)
for channel in channels)
if limited_access:
fields = [field for field in fields if field in self._PROMOTIONAL_FIELDS]
return fields
def get_related_slides(self, limit=20):
domain = [('website_published', '=', True), ('channel_id.visibility', '!=', 'private'), ('id', '!=', self.id)]
if self.category_id:
domain += [('category_id', '=', self.category_id.id)]
for record in self.search(domain, limit=limit):
yield record
def get_most_viewed_slides(self, limit=20):
for record in self.search([('website_published', '=', True), ('channel_id.visibility', '!=', 'private'), ('id', '!=', self.id)], limit=limit, order='total_views desc'):
yield record
def _post_publication(self):
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
for slide in self.filtered(lambda slide: slide.website_published):
publish_template = slide.channel_id.publish_template_id
html_body = publish_template.with_context({'base_url': base_url}).render_template(publish_template.body_html, 'slide.slide', slide.id)
slide.channel_id.message_post(body=html_body, subtype='website_slides.mt_channel_slide_published')
return True
@api.one
def send_share_email(self, email):
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
return self.channel_id.share_template_id.with_context({'email': email, 'base_url': base_url}).send_mail(self.id)
# --------------------------------------------------
# Parsing methods
# --------------------------------------------------
@api.model
def _fetch_data(self, base_url, data, content_type=False):
result = {'values': dict()}
try:
if data:
base_url = base_url + '?%s' % urlencode(data)
req = urllib2.Request(base_url)
content = urllib2.urlopen(req).read()
if content_type == 'json':
result['values'] = json.loads(content)
elif content_type in ('image', 'pdf'):
result['values'] = content.encode('base64')
else:
result['values'] = content
except urllib2.HTTPError as e:
result['error'] = e.read()
e.close()
return result
def _find_document_data_from_url(self, url):
expr = re.compile(r'^.*((youtu.be/)|(v/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*')
arg = expr.match(url)
document_id = arg and arg.group(7) or False
if document_id:
return ('youtube', document_id)
expr = re.compile(r'(^https:\/\/docs.google.com|^https:\/\/drive.google.com).*\/d\/([^\/]*)')
arg = expr.match(url)
document_id = arg and arg.group(2) or False
if document_id:
return ('google', document_id)
return (None, False)
def _parse_document_url(self, url, only_preview_fields=False):
document_source, document_id = self._find_document_data_from_url(url)
if document_source and hasattr(self, '_parse_%s_document' % document_source):
return getattr(self, '_parse_%s_document' % document_source)(document_id, only_preview_fields)
return {'error': _('Unknown document')}
def _parse_youtube_document(self, document_id, only_preview_fields):
key = self.env['ir.config_parameter'].sudo().get_param('website_slides.google_app_key')
fetch_res = self._fetch_data('https://www.googleapis.com/youtube/v3/videos', {'id': document_id, 'key': key, 'part': 'snippet', 'fields': 'items(id,snippet)'}, 'json')
if fetch_res.get('error'):
return fetch_res
values = {'slide_type': 'video', 'document_id': document_id}
youtube_values = fetch_res['values'].get('items', list(dict()))[0]
if youtube_values.get('snippet'):
snippet = youtube_values['snippet']
if only_preview_fields:
values.update({
'url_src': snippet['thumbnails']['high']['url'],
'title': snippet['title'],
'description': snippet['description']
})
return values
values.update({
'name': snippet['title'],
'image': self._fetch_data(snippet['thumbnails']['high']['url'], {}, 'image')['values'],
'description': snippet['description'],
})
return {'values': values}
@api.model
def _parse_google_document(self, document_id, only_preview_fields):
def get_slide_type(vals):
# TDE FIXME: WTF ??
image = Image.open(io.BytesIO(vals['image'].decode('base64')))
width, height = image.size
if height > width:
return 'document'
else:
return 'presentation'
key = self.env['ir.config_parameter'].sudo().get_param('website_slides.google_app_key')
fetch_res = self._fetch_data('https://www.googleapis.com/drive/v2/files/%s' % document_id, {'projection': 'BASIC', 'key': key}, "json")
if fetch_res.get('error'):
return fetch_res
google_values = fetch_res['values']
if only_preview_fields:
return {
'url_src': google_values['thumbnailLink'],
'title': google_values['title'],
}
values = {
'name': google_values['title'],
'image': self._fetch_data(google_values['thumbnailLink'].replace('=s220', ''), {}, 'image')['values'],
'mime_type': google_values['mimeType'],
'document_id': document_id,
}
if google_values['mimeType'].startswith('video/'):
values['slide_type'] = 'video'
elif google_values['mimeType'].startswith('image/'):
values['datas'] = values['image']
values['slide_type'] = 'infographic'
elif google_values['mimeType'].startswith('application/vnd.google-apps'):
values['datas'] = self._fetch_data(google_values['exportLinks']['application/pdf'], {}, 'pdf')['values']
values['slide_type'] = get_slide_type(values)
if google_values['exportLinks'].get('text/plain'):
values['index_content'] = self._fetch_data(google_values['exportLinks']['text/plain'], {})['values']
if google_values['exportLinks'].get('text/csv'):
values['index_content'] = self._fetch_data(google_values['exportLinks']['text/csv'], {})['values']
elif google_values['mimeType'] == 'application/pdf':
# TODO: Google Drive PDF document doesn't provide plain text transcript
values['datas'] = self._fetch_data(google_values['webContentLink'], {}, 'pdf')['values']
values['slide_type'] = get_slide_type(values)
return {'values': values}
@@ -0,0 +1,15 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_slide_slide_public,slide.slide.public,model_slide_slide,base.group_public,1,0,0,0
access_slide_slide_portal,slide.slide.portal,model_slide_slide,base.group_portal,1,1,1,0
access_slide_slide_user,slide.slide.user,model_slide_slide,base.group_user,1,1,1,1
access_slide_tag_public,slide.tag.public,model_slide_tag,base.group_public,1,0,0,0
access_slide_tag_portal,slide.tag.portal,model_slide_tag,base.group_portal,1,0,0,0
access_slide_tag_user,slide.tag.user,model_slide_tag,base.group_user,1,1,1,1
access_slide_channel_public,slide.channel.public,model_slide_channel,base.group_public,1,0,0,0
access_slide_channel_portal,slide.channel.portal,model_slide_channel,base.group_portal,1,0,0,0
access_slide_channel_user,slide.channel.user,model_slide_channel,base.group_user,1,1,1,1
access_slide_category_public,slide.category.public,model_slide_category,base.group_public,1,0,0,0
access_slide_category_portal,slide.category.portal,model_slide_category,base.group_portal,1,0,0,0
access_slide_category_user,slide.category.user,model_slide_category,base.group_user,1,1,1,1
access_slide_embed_all,slide.embed.all,model_slide_embed,,1,0,0,0
access_slide_embed_user,slide.embed.user,model_slide_embed,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_slide_slide_public slide.slide.public model_slide_slide base.group_public 1 0 0 0
3 access_slide_slide_portal slide.slide.portal model_slide_slide base.group_portal 1 1 1 0
4 access_slide_slide_user slide.slide.user model_slide_slide base.group_user 1 1 1 1
5 access_slide_tag_public slide.tag.public model_slide_tag base.group_public 1 0 0 0
6 access_slide_tag_portal slide.tag.portal model_slide_tag base.group_portal 1 0 0 0
7 access_slide_tag_user slide.tag.user model_slide_tag base.group_user 1 1 1 1
8 access_slide_channel_public slide.channel.public model_slide_channel base.group_public 1 0 0 0
9 access_slide_channel_portal slide.channel.portal model_slide_channel base.group_portal 1 0 0 0
10 access_slide_channel_user slide.channel.user model_slide_channel base.group_user 1 1 1 1
11 access_slide_category_public slide.category.public model_slide_category base.group_public 1 0 0 0
12 access_slide_category_portal slide.category.portal model_slide_category base.group_portal 1 0 0 0
13 access_slide_category_user slide.category.user model_slide_category base.group_user 1 1 1 1
14 access_slide_embed_all slide.embed.all model_slide_embed 1 0 0 0
15 access_slide_embed_user slide.embed.user model_slide_embed base.group_user 1 1 1 1
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record id="rule_slide_channel_global" model="ir.rule">
<field name="name">Channel: restriction by group and visibility</field>
<field name="model_id" ref="model_slide_channel"/>
<field name="global" eval="True"/>
<field name="domain_force">[
'|',
('visibility', 'in', ['public', 'partial']),
'&amp;',
('group_ids', 'in', [g.id for g in user.groups_id]),
('visibility', '=', 'private')]</field>
</record>
<record id="rule_slide_channel_public" model="ir.rule">
<field name="name">Channel: public: published only</field>
<field name="model_id" ref="model_slide_channel"/>
<field name="groups" eval="[(4, ref('base.group_public'))]"/>
<field name="domain_force">[('website_published', '=', True)]</field>
</record>
<record id="rule_slide_slide_global" model="ir.rule">
<field name="name">Slide: channel-related group restriction</field>
<field name="model_id" ref="model_slide_slide"/>
<field name="global" eval="True"/>
<field name="domain_force">[
'|',
('channel_id.visibility','in', ['public','partial']),
'&amp;',
('channel_id.group_ids', 'in', [g.id for g in user.groups_id]),
('channel_id.visibility', '=', 'private')]</field>
</record>
<record id="rule_slide_slide_public" model="ir.rule">
<field name="name">Slide: public: published only</field>
<field name="model_id" ref="model_slide_slide"/>
<field name="groups" eval="[(4, ref('base.group_public'))]"/>
<field name="domain_force">[('channel_id.website_published', '=', True), ('website_published', '=', True)]</field>
</record>
</data>
</openerp>
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

@@ -0,0 +1,88 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h2 class="oe_slogan">Presentations, Videos, Documents, Infographics</h2>
<h3 class="oe_slogan">Publish your presentations, videos, documents, infographics, etc.</h3>
</div>
<div class="oe_span6">
<div class="oe_demo oe_picture oe_screenshot">
<iframe width="361" height="200" frameborder="0" allowfullscreen="" src=""></iframe>
</div>
</div>
<div class="oe_span6">
<p class="oe_mt32">
Odoo presentations lets you upload files privately or publicly in the following file formats: PowerPoint, PDF, Keynote, Video or OpenDocument presentations.</p>
<p>This module originally meant to be used for businesses to share slides among employees more easily. It also provides users the ability to rate, comment, and share the uploaded content.
</p>
<div class="oe_centeralign oe_websiteonly">
<a class="oe_button oe_big oe_tacky" href="mailto:sales@openerp.com">Contact Us</a>
</div>
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h2 class="oe_slogan">Features</h2>
<h3 class="oe_slogan">Publish Presentation, Videos, Infographics or Documents</h3>
<div class="oe_span6">
<p class="oe_mt16">
<ul>
<li>Allow users to publish their presentations, videos, infographics as well as images</li>
<li>Publish publicly or privately</li>
<li>Share the content that matters to you with your colleagues, customers, friends and visitors</li>
<li>If you have any idea, story, research project, presentation, photo collection or
expert knowledge on a topic? Upload it and reach a wide audience with this application</li>
<li>Searching becomes super easy with tags, title and content of file</li>
<li>Propose related and most viewed documents
(presentations, videos, Infographics or images) to user</li>
</ul>
</p>
</div>
<div class="oe_span6">
<div class="oe_row_img oe_centered">
<img src="slides1.png" class="oe_picture oe_screenshot">
</div>
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h3 class="oe_slogan">Channel Management</h3>
<div class="oe_span6">
<div class="oe_row_img oe_centered">
<img src="slides2.png" class="oe_picture oe_screenshot">
</div>
</div>
<div class="oe_span6">
<p class="oe_mt32">
<ul>
<li>Use single or multiple channels</li>
<li>Privacy of a channel can be easily maintained</li>
<li>Organize your channels in categories</li>
</ul>
</p>
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<h3 class="oe_slogan">Embed anywhere</h3>
<div class="oe_span6">
<p class="oe_mt32">This application allows embedding into website and blogs</p>
<p>Easily shareable on LinkedIn, Twitter, Facebook and other popular social media platforms</p>
<p>Provides smart analytics like:</p>
<ul>
<li>Views count and traffic origin</li>
<li>Measure engagement for each upload: Know what's being downloaded, liked, commented on and emailed</li>
</ul>
</div>
<div class="oe_span6">
<div class="oe_row_img oe_centered">
<img src="slides3.png" class="oe_picture oe_screenshot">
</div>
</div>
</div>
</section>
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

@@ -0,0 +1,544 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals PDFJS, Util, isDict, isName, stringToPDFString, warn, Dict, Stream,
stringToBytes, assert, Promise, isArray, ObjectLoader, OperatorList,
isValidUrl, OPS, createPromiseCapability, AnnotationType */
'use strict';
var DEFAULT_ICON_SIZE = 22; // px
var SUPPORTED_TYPES = ['Link', 'Text', 'Widget'];
var Annotation = (function AnnotationClosure() {
// 12.5.5: Algorithm: Appearance streams
function getTransformMatrix(rect, bbox, matrix) {
var bounds = Util.getAxialAlignedBoundingBox(bbox, matrix);
var minX = bounds[0];
var minY = bounds[1];
var maxX = bounds[2];
var maxY = bounds[3];
if (minX === maxX || minY === maxY) {
// From real-life file, bbox was [0, 0, 0, 0]. In this case,
// just apply the transform for rect
return [1, 0, 0, 1, rect[0], rect[1]];
}
var xRatio = (rect[2] - rect[0]) / (maxX - minX);
var yRatio = (rect[3] - rect[1]) / (maxY - minY);
return [
xRatio,
0,
0,
yRatio,
rect[0] - minX * xRatio,
rect[1] - minY * yRatio
];
}
function getDefaultAppearance(dict) {
var appearanceState = dict.get('AP');
if (!isDict(appearanceState)) {
return;
}
var appearance;
var appearances = appearanceState.get('N');
if (isDict(appearances)) {
var as = dict.get('AS');
if (as && appearances.has(as.name)) {
appearance = appearances.get(as.name);
}
} else {
appearance = appearances;
}
return appearance;
}
function Annotation(params) {
var dict = params.dict;
var data = this.data = {};
data.subtype = dict.get('Subtype').name;
var rect = dict.get('Rect') || [0, 0, 0, 0];
data.rect = Util.normalizeRect(rect);
data.annotationFlags = dict.get('F');
var color = dict.get('C');
if (!color) {
// The PDF spec does not mention how a missing color array is interpreted.
// Adobe Reader seems to default to black in this case.
data.color = [0, 0, 0];
} else if (isArray(color)) {
switch (color.length) {
case 0:
// Empty array denotes transparent border.
data.color = null;
break;
case 1:
// TODO: implement DeviceGray
break;
case 3:
data.color = color;
break;
case 4:
// TODO: implement DeviceCMYK
break;
}
}
// Some types of annotations have border style dict which has more
// info than the border array
if (dict.has('BS')) {
var borderStyle = dict.get('BS');
data.borderWidth = borderStyle.has('W') ? borderStyle.get('W') : 1;
} else {
var borderArray = dict.get('Border') || [0, 0, 1];
data.borderWidth = borderArray[2] || 0;
// TODO: implement proper support for annotations with line dash patterns.
var dashArray = borderArray[3];
if (data.borderWidth > 0 && dashArray) {
if (!isArray(dashArray)) {
// Ignore the border if dashArray is not actually an array,
// this is consistent with the behaviour in Adobe Reader.
data.borderWidth = 0;
} else {
var dashArrayLength = dashArray.length;
if (dashArrayLength > 0) {
// According to the PDF specification: the elements in a dashArray
// shall be numbers that are nonnegative and not all equal to zero.
var isInvalid = false;
var numPositive = 0;
for (var i = 0; i < dashArrayLength; i++) {
var validNumber = (+dashArray[i] >= 0);
if (!validNumber) {
isInvalid = true;
break;
} else if (dashArray[i] > 0) {
numPositive++;
}
}
if (isInvalid || numPositive === 0) {
data.borderWidth = 0;
}
}
}
}
}
this.appearance = getDefaultAppearance(dict);
data.hasAppearance = !!this.appearance;
data.id = params.ref.num;
}
Annotation.prototype = {
getData: function Annotation_getData() {
return this.data;
},
isInvisible: function Annotation_isInvisible() {
var data = this.data;
if (data && SUPPORTED_TYPES.indexOf(data.subtype) !== -1) {
return false;
} else {
return !!(data &&
data.annotationFlags && // Default: not invisible
data.annotationFlags & 0x1); // Invisible
}
},
isViewable: function Annotation_isViewable() {
var data = this.data;
return !!(!this.isInvisible() &&
data &&
(!data.annotationFlags ||
!(data.annotationFlags & 0x22)) && // Hidden or NoView
data.rect); // rectangle is necessary
},
isPrintable: function Annotation_isPrintable() {
var data = this.data;
return !!(!this.isInvisible() &&
data &&
data.annotationFlags && // Default: not printable
data.annotationFlags & 0x4 && // Print
!(data.annotationFlags & 0x2) && // Hidden
data.rect); // rectangle is necessary
},
loadResources: function Annotation_loadResources(keys) {
return new Promise(function (resolve, reject) {
this.appearance.dict.getAsync('Resources').then(function (resources) {
if (!resources) {
resolve();
return;
}
var objectLoader = new ObjectLoader(resources.map,
keys,
resources.xref);
objectLoader.load().then(function() {
resolve(resources);
}, reject);
}, reject);
}.bind(this));
},
getOperatorList: function Annotation_getOperatorList(evaluator) {
if (!this.appearance) {
return Promise.resolve(new OperatorList());
}
var data = this.data;
var appearanceDict = this.appearance.dict;
var resourcesPromise = this.loadResources([
'ExtGState',
'ColorSpace',
'Pattern',
'Shading',
'XObject',
'Font'
// ProcSet
// Properties
]);
var bbox = appearanceDict.get('BBox') || [0, 0, 1, 1];
var matrix = appearanceDict.get('Matrix') || [1, 0, 0, 1, 0 ,0];
var transform = getTransformMatrix(data.rect, bbox, matrix);
var self = this;
return resourcesPromise.then(function(resources) {
var opList = new OperatorList();
opList.addOp(OPS.beginAnnotation, [data.rect, transform, matrix]);
return evaluator.getOperatorList(self.appearance, resources, opList).
then(function () {
opList.addOp(OPS.endAnnotation, []);
self.appearance.reset();
return opList;
});
});
}
};
Annotation.getConstructor =
function Annotation_getConstructor(subtype, fieldType) {
if (!subtype) {
return;
}
// TODO(mack): Implement FreeText annotations
if (subtype === 'Link') {
return LinkAnnotation;
} else if (subtype === 'Text') {
return TextAnnotation;
} else if (subtype === 'Widget') {
if (!fieldType) {
return;
}
if (fieldType === 'Tx') {
return TextWidgetAnnotation;
} else {
return WidgetAnnotation;
}
} else {
return Annotation;
}
};
Annotation.fromRef = function Annotation_fromRef(xref, ref) {
var dict = xref.fetchIfRef(ref);
if (!isDict(dict)) {
return;
}
var subtype = dict.get('Subtype');
subtype = isName(subtype) ? subtype.name : '';
if (!subtype) {
return;
}
var fieldType = Util.getInheritableProperty(dict, 'FT');
fieldType = isName(fieldType) ? fieldType.name : '';
var Constructor = Annotation.getConstructor(subtype, fieldType);
if (!Constructor) {
return;
}
var params = {
dict: dict,
ref: ref,
};
var annotation = new Constructor(params);
if (annotation.isViewable() || annotation.isPrintable()) {
return annotation;
} else {
if (SUPPORTED_TYPES.indexOf(subtype) === -1) {
warn('unimplemented annotation type: ' + subtype);
}
}
};
Annotation.appendToOperatorList = function Annotation_appendToOperatorList(
annotations, opList, pdfManager, partialEvaluator, intent) {
function reject(e) {
annotationsReadyCapability.reject(e);
}
var annotationsReadyCapability = createPromiseCapability();
var annotationPromises = [];
for (var i = 0, n = annotations.length; i < n; ++i) {
if (intent === 'display' && annotations[i].isViewable() ||
intent === 'print' && annotations[i].isPrintable()) {
annotationPromises.push(
annotations[i].getOperatorList(partialEvaluator));
}
}
Promise.all(annotationPromises).then(function(datas) {
opList.addOp(OPS.beginAnnotations, []);
for (var i = 0, n = datas.length; i < n; ++i) {
var annotOpList = datas[i];
opList.addOpList(annotOpList);
}
opList.addOp(OPS.endAnnotations, []);
annotationsReadyCapability.resolve();
}, reject);
return annotationsReadyCapability.promise;
};
return Annotation;
})();
var WidgetAnnotation = (function WidgetAnnotationClosure() {
function WidgetAnnotation(params) {
Annotation.call(this, params);
var dict = params.dict;
var data = this.data;
data.fieldValue = stringToPDFString(
Util.getInheritableProperty(dict, 'V') || '');
data.alternativeText = stringToPDFString(dict.get('TU') || '');
data.defaultAppearance = Util.getInheritableProperty(dict, 'DA') || '';
var fieldType = Util.getInheritableProperty(dict, 'FT');
data.fieldType = isName(fieldType) ? fieldType.name : '';
data.fieldFlags = Util.getInheritableProperty(dict, 'Ff') || 0;
this.fieldResources = Util.getInheritableProperty(dict, 'DR') || Dict.empty;
// Building the full field name by collecting the field and
// its ancestors 'T' data and joining them using '.'.
var fieldName = [];
var namedItem = dict;
var ref = params.ref;
while (namedItem) {
var parent = namedItem.get('Parent');
var parentRef = namedItem.getRaw('Parent');
var name = namedItem.get('T');
if (name) {
fieldName.unshift(stringToPDFString(name));
} else if (parent && ref) {
// The field name is absent, that means more than one field
// with the same name may exist. Replacing the empty name
// with the '`' plus index in the parent's 'Kids' array.
// This is not in the PDF spec but necessary to id the
// the input controls.
var kids = parent.get('Kids');
var j, jj;
for (j = 0, jj = kids.length; j < jj; j++) {
var kidRef = kids[j];
if (kidRef.num === ref.num && kidRef.gen === ref.gen) {
break;
}
}
fieldName.unshift('`' + j);
}
namedItem = parent;
ref = parentRef;
}
data.fullName = fieldName.join('.');
}
var parent = Annotation.prototype;
Util.inherit(WidgetAnnotation, Annotation, {
isViewable: function WidgetAnnotation_isViewable() {
if (this.data.fieldType === 'Sig') {
warn('unimplemented annotation type: Widget signature');
return false;
}
return parent.isViewable.call(this);
}
});
return WidgetAnnotation;
})();
var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() {
function TextWidgetAnnotation(params) {
WidgetAnnotation.call(this, params);
this.data.textAlignment = Util.getInheritableProperty(params.dict, 'Q');
this.data.annotationType = AnnotationType.WIDGET;
this.data.hasHtml = !this.data.hasAppearance && !!this.data.fieldValue;
}
Util.inherit(TextWidgetAnnotation, WidgetAnnotation, {
getOperatorList: function TextWidgetAnnotation_getOperatorList(evaluator) {
if (this.appearance) {
return Annotation.prototype.getOperatorList.call(this, evaluator);
}
var opList = new OperatorList();
var data = this.data;
// Even if there is an appearance stream, ignore it. This is the
// behaviour used by Adobe Reader.
if (!data.defaultAppearance) {
return Promise.resolve(opList);
}
var stream = new Stream(stringToBytes(data.defaultAppearance));
return evaluator.getOperatorList(stream, this.fieldResources, opList).
then(function () {
return opList;
});
}
});
return TextWidgetAnnotation;
})();
var InteractiveAnnotation = (function InteractiveAnnotationClosure() {
function InteractiveAnnotation(params) {
Annotation.call(this, params);
this.data.hasHtml = true;
}
Util.inherit(InteractiveAnnotation, Annotation, { });
return InteractiveAnnotation;
})();
var TextAnnotation = (function TextAnnotationClosure() {
function TextAnnotation(params) {
InteractiveAnnotation.call(this, params);
var dict = params.dict;
var data = this.data;
var content = dict.get('Contents');
var title = dict.get('T');
data.annotationType = AnnotationType.TEXT;
data.content = stringToPDFString(content || '');
data.title = stringToPDFString(title || '');
if (data.hasAppearance) {
data.name = 'NoIcon';
} else {
data.rect[1] = data.rect[3] - DEFAULT_ICON_SIZE;
data.rect[2] = data.rect[0] + DEFAULT_ICON_SIZE;
data.name = dict.has('Name') ? dict.get('Name').name : 'Note';
}
if (dict.has('C')) {
data.hasBgColor = true;
}
}
Util.inherit(TextAnnotation, InteractiveAnnotation, { });
return TextAnnotation;
})();
var LinkAnnotation = (function LinkAnnotationClosure() {
function LinkAnnotation(params) {
InteractiveAnnotation.call(this, params);
var dict = params.dict;
var data = this.data;
data.annotationType = AnnotationType.LINK;
var action = dict.get('A');
if (action) {
var linkType = action.get('S').name;
if (linkType === 'URI') {
var url = action.get('URI');
if (isName(url)) {
// Some bad PDFs do not put parentheses around relative URLs.
url = '/' + url.name;
} else if (url) {
url = addDefaultProtocolToUrl(url);
}
// TODO: pdf spec mentions urls can be relative to a Base
// entry in the dictionary.
if (!isValidUrl(url, false)) {
url = '';
}
data.url = url;
} else if (linkType === 'GoTo') {
data.dest = action.get('D');
} else if (linkType === 'GoToR') {
var urlDict = action.get('F');
if (isDict(urlDict)) {
// We assume that the 'url' is a Filspec dictionary
// and fetch the url without checking any further
url = urlDict.get('F') || '';
}
// TODO: pdf reference says that GoToR
// can also have 'NewWindow' attribute
if (!isValidUrl(url, false)) {
url = '';
}
data.url = url;
data.dest = action.get('D');
} else if (linkType === 'Named') {
data.action = action.get('N').name;
} else {
warn('unrecognized link type: ' + linkType);
}
} else if (dict.has('Dest')) {
// simple destination link
var dest = dict.get('Dest');
data.dest = isName(dest) ? dest.name : dest;
}
}
// Lets URLs beginning with 'www.' default to using the 'http://' protocol.
function addDefaultProtocolToUrl(url) {
if (url && url.indexOf('www.') === 0) {
return ('http://' + url);
}
return url;
}
Util.inherit(LinkAnnotation, InteractiveAnnotation, { });
return LinkAnnotation;
})();
@@ -0,0 +1,185 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
/* This class implements the QM Coder decoding as defined in
* JPEG 2000 Part I Final Committee Draft Version 1.0
* Annex C.3 Arithmetic decoding procedure
* available at http://www.jpeg.org/public/fcd15444-1.pdf
*
* The arithmetic decoder is used in conjunction with context models to decode
* JPEG2000 and JBIG2 streams.
*/
var ArithmeticDecoder = (function ArithmeticDecoderClosure() {
// Table C-2
var QeTable = [
{qe: 0x5601, nmps: 1, nlps: 1, switchFlag: 1},
{qe: 0x3401, nmps: 2, nlps: 6, switchFlag: 0},
{qe: 0x1801, nmps: 3, nlps: 9, switchFlag: 0},
{qe: 0x0AC1, nmps: 4, nlps: 12, switchFlag: 0},
{qe: 0x0521, nmps: 5, nlps: 29, switchFlag: 0},
{qe: 0x0221, nmps: 38, nlps: 33, switchFlag: 0},
{qe: 0x5601, nmps: 7, nlps: 6, switchFlag: 1},
{qe: 0x5401, nmps: 8, nlps: 14, switchFlag: 0},
{qe: 0x4801, nmps: 9, nlps: 14, switchFlag: 0},
{qe: 0x3801, nmps: 10, nlps: 14, switchFlag: 0},
{qe: 0x3001, nmps: 11, nlps: 17, switchFlag: 0},
{qe: 0x2401, nmps: 12, nlps: 18, switchFlag: 0},
{qe: 0x1C01, nmps: 13, nlps: 20, switchFlag: 0},
{qe: 0x1601, nmps: 29, nlps: 21, switchFlag: 0},
{qe: 0x5601, nmps: 15, nlps: 14, switchFlag: 1},
{qe: 0x5401, nmps: 16, nlps: 14, switchFlag: 0},
{qe: 0x5101, nmps: 17, nlps: 15, switchFlag: 0},
{qe: 0x4801, nmps: 18, nlps: 16, switchFlag: 0},
{qe: 0x3801, nmps: 19, nlps: 17, switchFlag: 0},
{qe: 0x3401, nmps: 20, nlps: 18, switchFlag: 0},
{qe: 0x3001, nmps: 21, nlps: 19, switchFlag: 0},
{qe: 0x2801, nmps: 22, nlps: 19, switchFlag: 0},
{qe: 0x2401, nmps: 23, nlps: 20, switchFlag: 0},
{qe: 0x2201, nmps: 24, nlps: 21, switchFlag: 0},
{qe: 0x1C01, nmps: 25, nlps: 22, switchFlag: 0},
{qe: 0x1801, nmps: 26, nlps: 23, switchFlag: 0},
{qe: 0x1601, nmps: 27, nlps: 24, switchFlag: 0},
{qe: 0x1401, nmps: 28, nlps: 25, switchFlag: 0},
{qe: 0x1201, nmps: 29, nlps: 26, switchFlag: 0},
{qe: 0x1101, nmps: 30, nlps: 27, switchFlag: 0},
{qe: 0x0AC1, nmps: 31, nlps: 28, switchFlag: 0},
{qe: 0x09C1, nmps: 32, nlps: 29, switchFlag: 0},
{qe: 0x08A1, nmps: 33, nlps: 30, switchFlag: 0},
{qe: 0x0521, nmps: 34, nlps: 31, switchFlag: 0},
{qe: 0x0441, nmps: 35, nlps: 32, switchFlag: 0},
{qe: 0x02A1, nmps: 36, nlps: 33, switchFlag: 0},
{qe: 0x0221, nmps: 37, nlps: 34, switchFlag: 0},
{qe: 0x0141, nmps: 38, nlps: 35, switchFlag: 0},
{qe: 0x0111, nmps: 39, nlps: 36, switchFlag: 0},
{qe: 0x0085, nmps: 40, nlps: 37, switchFlag: 0},
{qe: 0x0049, nmps: 41, nlps: 38, switchFlag: 0},
{qe: 0x0025, nmps: 42, nlps: 39, switchFlag: 0},
{qe: 0x0015, nmps: 43, nlps: 40, switchFlag: 0},
{qe: 0x0009, nmps: 44, nlps: 41, switchFlag: 0},
{qe: 0x0005, nmps: 45, nlps: 42, switchFlag: 0},
{qe: 0x0001, nmps: 45, nlps: 43, switchFlag: 0},
{qe: 0x5601, nmps: 46, nlps: 46, switchFlag: 0}
];
// C.3.5 Initialisation of the decoder (INITDEC)
function ArithmeticDecoder(data, start, end) {
this.data = data;
this.bp = start;
this.dataEnd = end;
this.chigh = data[start];
this.clow = 0;
this.byteIn();
this.chigh = ((this.chigh << 7) & 0xFFFF) | ((this.clow >> 9) & 0x7F);
this.clow = (this.clow << 7) & 0xFFFF;
this.ct -= 7;
this.a = 0x8000;
}
ArithmeticDecoder.prototype = {
// C.3.4 Compressed data input (BYTEIN)
byteIn: function ArithmeticDecoder_byteIn() {
var data = this.data;
var bp = this.bp;
if (data[bp] === 0xFF) {
var b1 = data[bp + 1];
if (b1 > 0x8F) {
this.clow += 0xFF00;
this.ct = 8;
} else {
bp++;
this.clow += (data[bp] << 9);
this.ct = 7;
this.bp = bp;
}
} else {
bp++;
this.clow += bp < this.dataEnd ? (data[bp] << 8) : 0xFF00;
this.ct = 8;
this.bp = bp;
}
if (this.clow > 0xFFFF) {
this.chigh += (this.clow >> 16);
this.clow &= 0xFFFF;
}
},
// C.3.2 Decoding a decision (DECODE)
readBit: function ArithmeticDecoder_readBit(contexts, pos) {
// contexts are packed into 1 byte:
// highest 7 bits carry cx.index, lowest bit carries cx.mps
var cx_index = contexts[pos] >> 1, cx_mps = contexts[pos] & 1;
var qeTableIcx = QeTable[cx_index];
var qeIcx = qeTableIcx.qe;
var d;
var a = this.a - qeIcx;
if (this.chigh < qeIcx) {
// exchangeLps
if (a < qeIcx) {
a = qeIcx;
d = cx_mps;
cx_index = qeTableIcx.nmps;
} else {
a = qeIcx;
d = 1 ^ cx_mps;
if (qeTableIcx.switchFlag === 1) {
cx_mps = d;
}
cx_index = qeTableIcx.nlps;
}
} else {
this.chigh -= qeIcx;
if ((a & 0x8000) !== 0) {
this.a = a;
return cx_mps;
}
// exchangeMps
if (a < qeIcx) {
d = 1 ^ cx_mps;
if (qeTableIcx.switchFlag === 1) {
cx_mps = d;
}
cx_index = qeTableIcx.nlps;
} else {
d = cx_mps;
cx_index = qeTableIcx.nmps;
}
}
// C.3.3 renormD;
do {
if (this.ct === 0) {
this.byteIn();
}
a <<= 1;
this.chigh = ((this.chigh << 1) & 0xFFFF) | ((this.clow >> 15) & 1);
this.clow = (this.clow << 1) & 0xFFFF;
this.ct--;
} while ((a & 0x8000) === 0);
this.a = a;
contexts[pos] = cx_index << 1 | cx_mps;
return d;
}
};
return ArithmeticDecoder;
})();
@@ -0,0 +1,427 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* globals PDFJS */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var bidi = PDFJS.bidi = (function bidiClosure() {
// Character types for symbols from 0000 to 00FF.
var baseTypes = [
'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'S', 'B', 'S', 'WS',
'B', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN',
'BN', 'BN', 'B', 'B', 'B', 'S', 'WS', 'ON', 'ON', 'ET', 'ET', 'ET', 'ON',
'ON', 'ON', 'ON', 'ON', 'ON', 'CS', 'ON', 'CS', 'ON', 'EN', 'EN', 'EN',
'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'ON', 'ON', 'ON', 'ON', 'ON',
'ON', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'ON', 'ON',
'ON', 'ON', 'ON', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
'L', 'ON', 'ON', 'ON', 'ON', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'B', 'BN',
'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN',
'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN',
'BN', 'CS', 'ON', 'ET', 'ET', 'ET', 'ET', 'ON', 'ON', 'ON', 'ON', 'L', 'ON',
'ON', 'ON', 'ON', 'ON', 'ET', 'ET', 'EN', 'EN', 'ON', 'L', 'ON', 'ON', 'ON',
'EN', 'L', 'ON', 'ON', 'ON', 'ON', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
'L', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
'L', 'L', 'L', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L'
];
// Character types for symbols from 0600 to 06FF
var arabicTypes = [
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'CS', 'AL', 'ON', 'ON', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'AL',
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AL', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM',
'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN',
'AN', 'ET', 'AN', 'AN', 'AL', 'AL', 'AL', 'NSM', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'AL', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM',
'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'ON', 'NSM',
'NSM', 'NSM', 'NSM', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL'
];
function isOdd(i) {
return (i & 1) !== 0;
}
function isEven(i) {
return (i & 1) === 0;
}
function findUnequal(arr, start, value) {
for (var j = start, jj = arr.length; j < jj; ++j) {
if (arr[j] !== value) {
return j;
}
}
return j;
}
function setValues(arr, start, end, value) {
for (var j = start; j < end; ++j) {
arr[j] = value;
}
}
function reverseValues(arr, start, end) {
for (var i = start, j = end - 1; i < j; ++i, --j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
function createBidiText(str, isLTR, vertical) {
return {
str: str,
dir: (vertical ? 'ttb' : (isLTR ? 'ltr' : 'rtl'))
};
}
// These are used in bidi(), which is called frequently. We re-use them on
// each call to avoid unnecessary allocations.
var chars = [];
var types = [];
function bidi(str, startLevel, vertical) {
var isLTR = true;
var strLength = str.length;
if (strLength === 0 || vertical) {
return createBidiText(str, isLTR, vertical);
}
// Get types and fill arrays
chars.length = strLength;
types.length = strLength;
var numBidi = 0;
var i, ii;
for (i = 0; i < strLength; ++i) {
chars[i] = str.charAt(i);
var charCode = str.charCodeAt(i);
var charType = 'L';
if (charCode <= 0x00ff) {
charType = baseTypes[charCode];
} else if (0x0590 <= charCode && charCode <= 0x05f4) {
charType = 'R';
} else if (0x0600 <= charCode && charCode <= 0x06ff) {
charType = arabicTypes[charCode & 0xff];
} else if (0x0700 <= charCode && charCode <= 0x08AC) {
charType = 'AL';
}
if (charType === 'R' || charType === 'AL' || charType === 'AN') {
numBidi++;
}
types[i] = charType;
}
// Detect the bidi method
// - If there are no rtl characters then no bidi needed
// - If less than 30% chars are rtl then string is primarily ltr
// - If more than 30% chars are rtl then string is primarily rtl
if (numBidi === 0) {
isLTR = true;
return createBidiText(str, isLTR);
}
if (startLevel === -1) {
if ((strLength / numBidi) < 0.3) {
isLTR = true;
startLevel = 0;
} else {
isLTR = false;
startLevel = 1;
}
}
var levels = [];
for (i = 0; i < strLength; ++i) {
levels[i] = startLevel;
}
/*
X1-X10: skip most of this, since we are NOT doing the embeddings.
*/
var e = (isOdd(startLevel) ? 'R' : 'L');
var sor = e;
var eor = sor;
/*
W1. Examine each non-spacing mark (NSM) in the level run, and change the
type of the NSM to the type of the previous character. If the NSM is at the
start of the level run, it will get the type of sor.
*/
var lastType = sor;
for (i = 0; i < strLength; ++i) {
if (types[i] === 'NSM') {
types[i] = lastType;
} else {
lastType = types[i];
}
}
/*
W2. Search backwards from each instance of a European number until the
first strong type (R, L, AL, or sor) is found. If an AL is found, change
the type of the European number to Arabic number.
*/
lastType = sor;
var t;
for (i = 0; i < strLength; ++i) {
t = types[i];
if (t === 'EN') {
types[i] = (lastType === 'AL') ? 'AN' : 'EN';
} else if (t === 'R' || t === 'L' || t === 'AL') {
lastType = t;
}
}
/*
W3. Change all ALs to R.
*/
for (i = 0; i < strLength; ++i) {
t = types[i];
if (t === 'AL') {
types[i] = 'R';
}
}
/*
W4. A single European separator between two European numbers changes to a
European number. A single common separator between two numbers of the same
type changes to that type:
*/
for (i = 1; i < strLength - 1; ++i) {
if (types[i] === 'ES' && types[i - 1] === 'EN' && types[i + 1] === 'EN') {
types[i] = 'EN';
}
if (types[i] === 'CS' &&
(types[i - 1] === 'EN' || types[i - 1] === 'AN') &&
types[i + 1] === types[i - 1]) {
types[i] = types[i - 1];
}
}
/*
W5. A sequence of European terminators adjacent to European numbers changes
to all European numbers:
*/
for (i = 0; i < strLength; ++i) {
if (types[i] === 'EN') {
// do before
var j;
for (j = i - 1; j >= 0; --j) {
if (types[j] !== 'ET') {
break;
}
types[j] = 'EN';
}
// do after
for (j = i + 1; j < strLength; --j) {
if (types[j] !== 'ET') {
break;
}
types[j] = 'EN';
}
}
}
/*
W6. Otherwise, separators and terminators change to Other Neutral:
*/
for (i = 0; i < strLength; ++i) {
t = types[i];
if (t === 'WS' || t === 'ES' || t === 'ET' || t === 'CS') {
types[i] = 'ON';
}
}
/*
W7. Search backwards from each instance of a European number until the
first strong type (R, L, or sor) is found. If an L is found, then change
the type of the European number to L.
*/
lastType = sor;
for (i = 0; i < strLength; ++i) {
t = types[i];
if (t === 'EN') {
types[i] = ((lastType === 'L') ? 'L' : 'EN');
} else if (t === 'R' || t === 'L') {
lastType = t;
}
}
/*
N1. A sequence of neutrals takes the direction of the surrounding strong
text if the text on both sides has the same direction. European and Arabic
numbers are treated as though they were R. Start-of-level-run (sor) and
end-of-level-run (eor) are used at level run boundaries.
*/
for (i = 0; i < strLength; ++i) {
if (types[i] === 'ON') {
var end = findUnequal(types, i + 1, 'ON');
var before = sor;
if (i > 0) {
before = types[i - 1];
}
var after = eor;
if (end + 1 < strLength) {
after = types[end + 1];
}
if (before !== 'L') {
before = 'R';
}
if (after !== 'L') {
after = 'R';
}
if (before === after) {
setValues(types, i, end, before);
}
i = end - 1; // reset to end (-1 so next iteration is ok)
}
}
/*
N2. Any remaining neutrals take the embedding direction.
*/
for (i = 0; i < strLength; ++i) {
if (types[i] === 'ON') {
types[i] = e;
}
}
/*
I1. For all characters with an even (left-to-right) embedding direction,
those of type R go up one level and those of type AN or EN go up two
levels.
I2. For all characters with an odd (right-to-left) embedding direction,
those of type L, EN or AN go up one level.
*/
for (i = 0; i < strLength; ++i) {
t = types[i];
if (isEven(levels[i])) {
if (t === 'R') {
levels[i] += 1;
} else if (t === 'AN' || t === 'EN') {
levels[i] += 2;
}
} else { // isOdd
if (t === 'L' || t === 'AN' || t === 'EN') {
levels[i] += 1;
}
}
}
/*
L1. On each line, reset the embedding level of the following characters to
the paragraph embedding level:
segment separators,
paragraph separators,
any sequence of whitespace characters preceding a segment separator or
paragraph separator, and any sequence of white space characters at the end
of the line.
*/
// don't bother as text is only single line
/*
L2. From the highest level found in the text to the lowest odd level on
each line, reverse any contiguous sequence of characters that are at that
level or higher.
*/
// find highest level & lowest odd level
var highestLevel = -1;
var lowestOddLevel = 99;
var level;
for (i = 0, ii = levels.length; i < ii; ++i) {
level = levels[i];
if (highestLevel < level) {
highestLevel = level;
}
if (lowestOddLevel > level && isOdd(level)) {
lowestOddLevel = level;
}
}
// now reverse between those limits
for (level = highestLevel; level >= lowestOddLevel; --level) {
// find segments to reverse
var start = -1;
for (i = 0, ii = levels.length; i < ii; ++i) {
if (levels[i] < level) {
if (start >= 0) {
reverseValues(chars, start, i);
start = -1;
}
} else if (start < 0) {
start = i;
}
}
if (start >= 0) {
reverseValues(chars, start, levels.length);
}
}
/*
L3. Combining marks applied to a right-to-left base character will at this
point precede their base character. If the rendering engine expects them to
follow the base characters in the final display process, then the ordering
of the marks and the base character must be reversed.
*/
// don't bother for now
/*
L4. A character that possesses the mirrored property as specified by
Section 4.7, Mirrored, must be depicted by a mirrored glyph if the resolved
directionality of that character is R.
*/
// don't mirror as characters are already mirrored in the pdf
// Finally, return string
var result = '';
for (i = 0, ii = chars.length; i < ii; ++i) {
var ch = chars[i];
if (ch !== '<' && ch !== '>') {
result += ch;
}
}
return createBidiText(result, isLTR);
}
return bidi;
})();
@@ -0,0 +1,119 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var ISOAdobeCharset = [
'.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar',
'percent', 'ampersand', 'quoteright', 'parenleft', 'parenright',
'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero',
'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight',
'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', 'question',
'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore',
'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'braceleft', 'bar', 'braceright', 'asciitilde', 'exclamdown', 'cent',
'sterling', 'fraction', 'yen', 'florin', 'section', 'currency',
'quotesingle', 'quotedblleft', 'guillemotleft', 'guilsinglleft',
'guilsinglright', 'fi', 'fl', 'endash', 'dagger', 'daggerdbl',
'periodcentered', 'paragraph', 'bullet', 'quotesinglbase',
'quotedblbase', 'quotedblright', 'guillemotright', 'ellipsis',
'perthousand', 'questiondown', 'grave', 'acute', 'circumflex', 'tilde',
'macron', 'breve', 'dotaccent', 'dieresis', 'ring', 'cedilla',
'hungarumlaut', 'ogonek', 'caron', 'emdash', 'AE', 'ordfeminine',
'Lslash', 'Oslash', 'OE', 'ordmasculine', 'ae', 'dotlessi', 'lslash',
'oslash', 'oe', 'germandbls', 'onesuperior', 'logicalnot', 'mu',
'trademark', 'Eth', 'onehalf', 'plusminus', 'Thorn', 'onequarter',
'divide', 'brokenbar', 'degree', 'thorn', 'threequarters', 'twosuperior',
'registered', 'minus', 'eth', 'multiply', 'threesuperior', 'copyright',
'Aacute', 'Acircumflex', 'Adieresis', 'Agrave', 'Aring', 'Atilde',
'Ccedilla', 'Eacute', 'Ecircumflex', 'Edieresis', 'Egrave', 'Iacute',
'Icircumflex', 'Idieresis', 'Igrave', 'Ntilde', 'Oacute', 'Ocircumflex',
'Odieresis', 'Ograve', 'Otilde', 'Scaron', 'Uacute', 'Ucircumflex',
'Udieresis', 'Ugrave', 'Yacute', 'Ydieresis', 'Zcaron', 'aacute',
'acircumflex', 'adieresis', 'agrave', 'aring', 'atilde', 'ccedilla',
'eacute', 'ecircumflex', 'edieresis', 'egrave', 'iacute', 'icircumflex',
'idieresis', 'igrave', 'ntilde', 'oacute', 'ocircumflex', 'odieresis',
'ograve', 'otilde', 'scaron', 'uacute', 'ucircumflex', 'udieresis',
'ugrave', 'yacute', 'ydieresis', 'zcaron'
];
var ExpertCharset = [
'.notdef', 'space', 'exclamsmall', 'Hungarumlautsmall', 'dollaroldstyle',
'dollarsuperior', 'ampersandsmall', 'Acutesmall', 'parenleftsuperior',
'parenrightsuperior', 'twodotenleader', 'onedotenleader', 'comma',
'hyphen', 'period', 'fraction', 'zerooldstyle', 'oneoldstyle',
'twooldstyle', 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle',
'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle',
'colon', 'semicolon', 'commasuperior', 'threequartersemdash',
'periodsuperior', 'questionsmall', 'asuperior', 'bsuperior',
'centsuperior', 'dsuperior', 'esuperior', 'isuperior', 'lsuperior',
'msuperior', 'nsuperior', 'osuperior', 'rsuperior', 'ssuperior',
'tsuperior', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'parenleftinferior',
'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall',
'Asmall', 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall',
'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall',
'Osmall', 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall',
'Vsmall', 'Wsmall', 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary',
'onefitted', 'rupiah', 'Tildesmall', 'exclamdownsmall', 'centoldstyle',
'Lslashsmall', 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall',
'Brevesmall', 'Caronsmall', 'Dotaccentsmall', 'Macronsmall',
'figuredash', 'hypheninferior', 'Ogoneksmall', 'Ringsmall',
'Cedillasmall', 'onequarter', 'onehalf', 'threequarters',
'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths',
'seveneighths', 'onethird', 'twothirds', 'zerosuperior', 'onesuperior',
'twosuperior', 'threesuperior', 'foursuperior', 'fivesuperior',
'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior',
'zeroinferior', 'oneinferior', 'twoinferior', 'threeinferior',
'fourinferior', 'fiveinferior', 'sixinferior', 'seveninferior',
'eightinferior', 'nineinferior', 'centinferior', 'dollarinferior',
'periodinferior', 'commainferior', 'Agravesmall', 'Aacutesmall',
'Acircumflexsmall', 'Atildesmall', 'Adieresissmall', 'Aringsmall',
'AEsmall', 'Ccedillasmall', 'Egravesmall', 'Eacutesmall',
'Ecircumflexsmall', 'Edieresissmall', 'Igravesmall', 'Iacutesmall',
'Icircumflexsmall', 'Idieresissmall', 'Ethsmall', 'Ntildesmall',
'Ogravesmall', 'Oacutesmall', 'Ocircumflexsmall', 'Otildesmall',
'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall', 'Uacutesmall',
'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall', 'Thornsmall',
'Ydieresissmall'
];
var ExpertSubsetCharset = [
'.notdef', 'space', 'dollaroldstyle', 'dollarsuperior',
'parenleftsuperior', 'parenrightsuperior', 'twodotenleader',
'onedotenleader', 'comma', 'hyphen', 'period', 'fraction',
'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle',
'fouroldstyle', 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle',
'eightoldstyle', 'nineoldstyle', 'colon', 'semicolon', 'commasuperior',
'threequartersemdash', 'periodsuperior', 'asuperior', 'bsuperior',
'centsuperior', 'dsuperior', 'esuperior', 'isuperior', 'lsuperior',
'msuperior', 'nsuperior', 'osuperior', 'rsuperior', 'ssuperior',
'tsuperior', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'parenleftinferior',
'parenrightinferior', 'hyphensuperior', 'colonmonetary', 'onefitted',
'rupiah', 'centoldstyle', 'figuredash', 'hypheninferior', 'onequarter',
'onehalf', 'threequarters', 'oneeighth', 'threeeighths', 'fiveeighths',
'seveneighths', 'onethird', 'twothirds', 'zerosuperior', 'onesuperior',
'twosuperior', 'threesuperior', 'foursuperior', 'fivesuperior',
'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior',
'zeroinferior', 'oneinferior', 'twoinferior', 'threeinferior',
'fourinferior', 'fiveinferior', 'sixinferior', 'seveninferior',
'eightinferior', 'nineinferior', 'centinferior', 'dollarinferior',
'periodinferior', 'commainferior'
];
@@ -0,0 +1,563 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals assert, MissingDataException, isInt, NetworkManager, Promise,
isEmptyObj, createPromiseCapability */
'use strict';
var ChunkedStream = (function ChunkedStreamClosure() {
function ChunkedStream(length, chunkSize, manager) {
this.bytes = new Uint8Array(length);
this.start = 0;
this.pos = 0;
this.end = length;
this.chunkSize = chunkSize;
this.loadedChunks = [];
this.numChunksLoaded = 0;
this.numChunks = Math.ceil(length / chunkSize);
this.manager = manager;
this.progressiveDataLength = 0;
this.lastSuccessfulEnsureByteChunk = -1; // a single-entry cache
}
// required methods for a stream. if a particular stream does not
// implement these, an error should be thrown
ChunkedStream.prototype = {
getMissingChunks: function ChunkedStream_getMissingChunks() {
var chunks = [];
for (var chunk = 0, n = this.numChunks; chunk < n; ++chunk) {
if (!this.loadedChunks[chunk]) {
chunks.push(chunk);
}
}
return chunks;
},
getBaseStreams: function ChunkedStream_getBaseStreams() {
return [this];
},
allChunksLoaded: function ChunkedStream_allChunksLoaded() {
return this.numChunksLoaded === this.numChunks;
},
onReceiveData: function ChunkedStream_onReceiveData(begin, chunk) {
var end = begin + chunk.byteLength;
assert(begin % this.chunkSize === 0, 'Bad begin offset: ' + begin);
// Using this.length is inaccurate here since this.start can be moved
// See ChunkedStream.moveStart()
var length = this.bytes.length;
assert(end % this.chunkSize === 0 || end === length,
'Bad end offset: ' + end);
this.bytes.set(new Uint8Array(chunk), begin);
var chunkSize = this.chunkSize;
var beginChunk = Math.floor(begin / chunkSize);
var endChunk = Math.floor((end - 1) / chunkSize) + 1;
var curChunk;
for (curChunk = beginChunk; curChunk < endChunk; ++curChunk) {
if (!this.loadedChunks[curChunk]) {
this.loadedChunks[curChunk] = true;
++this.numChunksLoaded;
}
}
},
onReceiveProgressiveData:
function ChunkedStream_onReceiveProgressiveData(data) {
var position = this.progressiveDataLength;
var beginChunk = Math.floor(position / this.chunkSize);
this.bytes.set(new Uint8Array(data), position);
position += data.byteLength;
this.progressiveDataLength = position;
var endChunk = position >= this.end ? this.numChunks :
Math.floor(position / this.chunkSize);
var curChunk;
for (curChunk = beginChunk; curChunk < endChunk; ++curChunk) {
if (!this.loadedChunks[curChunk]) {
this.loadedChunks[curChunk] = true;
++this.numChunksLoaded;
}
}
},
ensureByte: function ChunkedStream_ensureByte(pos) {
var chunk = Math.floor(pos / this.chunkSize);
if (chunk === this.lastSuccessfulEnsureByteChunk) {
return;
}
if (!this.loadedChunks[chunk]) {
throw new MissingDataException(pos, pos + 1);
}
this.lastSuccessfulEnsureByteChunk = chunk;
},
ensureRange: function ChunkedStream_ensureRange(begin, end) {
if (begin >= end) {
return;
}
if (end <= this.progressiveDataLength) {
return;
}
var chunkSize = this.chunkSize;
var beginChunk = Math.floor(begin / chunkSize);
var endChunk = Math.floor((end - 1) / chunkSize) + 1;
for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
if (!this.loadedChunks[chunk]) {
throw new MissingDataException(begin, end);
}
}
},
nextEmptyChunk: function ChunkedStream_nextEmptyChunk(beginChunk) {
var chunk, n;
for (chunk = beginChunk, n = this.numChunks; chunk < n; ++chunk) {
if (!this.loadedChunks[chunk]) {
return chunk;
}
}
// Wrap around to beginning
for (chunk = 0; chunk < beginChunk; ++chunk) {
if (!this.loadedChunks[chunk]) {
return chunk;
}
}
return null;
},
hasChunk: function ChunkedStream_hasChunk(chunk) {
return !!this.loadedChunks[chunk];
},
get length() {
return this.end - this.start;
},
get isEmpty() {
return this.length === 0;
},
getByte: function ChunkedStream_getByte() {
var pos = this.pos;
if (pos >= this.end) {
return -1;
}
this.ensureByte(pos);
return this.bytes[this.pos++];
},
getUint16: function ChunkedStream_getUint16() {
var b0 = this.getByte();
var b1 = this.getByte();
if (b0 === -1 || b1 === -1) {
return -1;
}
return (b0 << 8) + b1;
},
getInt32: function ChunkedStream_getInt32() {
var b0 = this.getByte();
var b1 = this.getByte();
var b2 = this.getByte();
var b3 = this.getByte();
return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3;
},
// returns subarray of original buffer
// should only be read
getBytes: function ChunkedStream_getBytes(length) {
var bytes = this.bytes;
var pos = this.pos;
var strEnd = this.end;
if (!length) {
this.ensureRange(pos, strEnd);
return bytes.subarray(pos, strEnd);
}
var end = pos + length;
if (end > strEnd) {
end = strEnd;
}
this.ensureRange(pos, end);
this.pos = end;
return bytes.subarray(pos, end);
},
peekByte: function ChunkedStream_peekByte() {
var peekedByte = this.getByte();
this.pos--;
return peekedByte;
},
peekBytes: function ChunkedStream_peekBytes(length) {
var bytes = this.getBytes(length);
this.pos -= bytes.length;
return bytes;
},
getByteRange: function ChunkedStream_getBytes(begin, end) {
this.ensureRange(begin, end);
return this.bytes.subarray(begin, end);
},
skip: function ChunkedStream_skip(n) {
if (!n) {
n = 1;
}
this.pos += n;
},
reset: function ChunkedStream_reset() {
this.pos = this.start;
},
moveStart: function ChunkedStream_moveStart() {
this.start = this.pos;
},
makeSubStream: function ChunkedStream_makeSubStream(start, length, dict) {
this.ensureRange(start, start + length);
function ChunkedStreamSubstream() {}
ChunkedStreamSubstream.prototype = Object.create(this);
ChunkedStreamSubstream.prototype.getMissingChunks = function() {
var chunkSize = this.chunkSize;
var beginChunk = Math.floor(this.start / chunkSize);
var endChunk = Math.floor((this.end - 1) / chunkSize) + 1;
var missingChunks = [];
for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
if (!this.loadedChunks[chunk]) {
missingChunks.push(chunk);
}
}
return missingChunks;
};
var subStream = new ChunkedStreamSubstream();
subStream.pos = subStream.start = start;
subStream.end = start + length || this.end;
subStream.dict = dict;
return subStream;
},
isStream: true
};
return ChunkedStream;
})();
var ChunkedStreamManager = (function ChunkedStreamManagerClosure() {
function ChunkedStreamManager(length, chunkSize, url, args) {
this.stream = new ChunkedStream(length, chunkSize, this);
this.length = length;
this.chunkSize = chunkSize;
this.url = url;
this.disableAutoFetch = args.disableAutoFetch;
var msgHandler = this.msgHandler = args.msgHandler;
if (args.chunkedViewerLoading) {
msgHandler.on('OnDataRange', this.onReceiveData.bind(this));
msgHandler.on('OnDataProgress', this.onProgress.bind(this));
this.sendRequest = function ChunkedStreamManager_sendRequest(begin, end) {
msgHandler.send('RequestDataRange', { begin: begin, end: end });
};
} else {
var getXhr = function getXhr() {
//#if B2G
// return new XMLHttpRequest({ mozSystem: true });
//#else
return new XMLHttpRequest();
//#endif
};
this.networkManager = new NetworkManager(this.url, {
getXhr: getXhr,
httpHeaders: args.httpHeaders,
withCredentials: args.withCredentials
});
this.sendRequest = function ChunkedStreamManager_sendRequest(begin, end) {
this.networkManager.requestRange(begin, end, {
onDone: this.onReceiveData.bind(this),
onProgress: this.onProgress.bind(this)
});
};
}
this.currRequestId = 0;
this.chunksNeededByRequest = {};
this.requestsByChunk = {};
this.callbacksByRequest = {};
this.progressiveDataLength = 0;
this._loadedStreamCapability = createPromiseCapability();
if (args.initialData) {
this.onReceiveData({chunk: args.initialData});
}
}
ChunkedStreamManager.prototype = {
onLoadedStream: function ChunkedStreamManager_getLoadedStream() {
return this._loadedStreamCapability.promise;
},
// Get all the chunks that are not yet loaded and groups them into
// contiguous ranges to load in as few requests as possible
requestAllChunks: function ChunkedStreamManager_requestAllChunks() {
var missingChunks = this.stream.getMissingChunks();
this.requestChunks(missingChunks);
return this._loadedStreamCapability.promise;
},
requestChunks: function ChunkedStreamManager_requestChunks(chunks,
callback) {
var requestId = this.currRequestId++;
var chunksNeeded;
var i, ii;
this.chunksNeededByRequest[requestId] = chunksNeeded = {};
for (i = 0, ii = chunks.length; i < ii; i++) {
if (!this.stream.hasChunk(chunks[i])) {
chunksNeeded[chunks[i]] = true;
}
}
if (isEmptyObj(chunksNeeded)) {
if (callback) {
callback();
}
return;
}
this.callbacksByRequest[requestId] = callback;
var chunksToRequest = [];
for (var chunk in chunksNeeded) {
chunk = chunk | 0;
if (!(chunk in this.requestsByChunk)) {
this.requestsByChunk[chunk] = [];
chunksToRequest.push(chunk);
}
this.requestsByChunk[chunk].push(requestId);
}
if (!chunksToRequest.length) {
return;
}
var groupedChunksToRequest = this.groupChunks(chunksToRequest);
for (i = 0; i < groupedChunksToRequest.length; ++i) {
var groupedChunk = groupedChunksToRequest[i];
var begin = groupedChunk.beginChunk * this.chunkSize;
var end = Math.min(groupedChunk.endChunk * this.chunkSize, this.length);
this.sendRequest(begin, end);
}
},
getStream: function ChunkedStreamManager_getStream() {
return this.stream;
},
// Loads any chunks in the requested range that are not yet loaded
requestRange: function ChunkedStreamManager_requestRange(
begin, end, callback) {
end = Math.min(end, this.length);
var beginChunk = this.getBeginChunk(begin);
var endChunk = this.getEndChunk(end);
var chunks = [];
for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
chunks.push(chunk);
}
this.requestChunks(chunks, callback);
},
requestRanges: function ChunkedStreamManager_requestRanges(ranges,
callback) {
ranges = ranges || [];
var chunksToRequest = [];
for (var i = 0; i < ranges.length; i++) {
var beginChunk = this.getBeginChunk(ranges[i].begin);
var endChunk = this.getEndChunk(ranges[i].end);
for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
if (chunksToRequest.indexOf(chunk) < 0) {
chunksToRequest.push(chunk);
}
}
}
chunksToRequest.sort(function(a, b) { return a - b; });
this.requestChunks(chunksToRequest, callback);
},
// Groups a sorted array of chunks into as few continguous larger
// chunks as possible
groupChunks: function ChunkedStreamManager_groupChunks(chunks) {
var groupedChunks = [];
var beginChunk = -1;
var prevChunk = -1;
for (var i = 0; i < chunks.length; ++i) {
var chunk = chunks[i];
if (beginChunk < 0) {
beginChunk = chunk;
}
if (prevChunk >= 0 && prevChunk + 1 !== chunk) {
groupedChunks.push({ beginChunk: beginChunk,
endChunk: prevChunk + 1 });
beginChunk = chunk;
}
if (i + 1 === chunks.length) {
groupedChunks.push({ beginChunk: beginChunk,
endChunk: chunk + 1 });
}
prevChunk = chunk;
}
return groupedChunks;
},
onProgress: function ChunkedStreamManager_onProgress(args) {
var bytesLoaded = (this.stream.numChunksLoaded * this.chunkSize +
args.loaded);
this.msgHandler.send('DocProgress', {
loaded: bytesLoaded,
total: this.length
});
},
onReceiveData: function ChunkedStreamManager_onReceiveData(args) {
var chunk = args.chunk;
var isProgressive = args.begin === undefined;
var begin = isProgressive ? this.progressiveDataLength : args.begin;
var end = begin + chunk.byteLength;
var beginChunk = Math.floor(begin / this.chunkSize);
var endChunk = end < this.length ? Math.floor(end / this.chunkSize) :
Math.ceil(end / this.chunkSize);
if (isProgressive) {
this.stream.onReceiveProgressiveData(chunk);
this.progressiveDataLength = end;
} else {
this.stream.onReceiveData(begin, chunk);
}
if (this.stream.allChunksLoaded()) {
this._loadedStreamCapability.resolve(this.stream);
}
var loadedRequests = [];
var i, requestId;
for (chunk = beginChunk; chunk < endChunk; ++chunk) {
// The server might return more chunks than requested
var requestIds = this.requestsByChunk[chunk] || [];
delete this.requestsByChunk[chunk];
for (i = 0; i < requestIds.length; ++i) {
requestId = requestIds[i];
var chunksNeeded = this.chunksNeededByRequest[requestId];
if (chunk in chunksNeeded) {
delete chunksNeeded[chunk];
}
if (!isEmptyObj(chunksNeeded)) {
continue;
}
loadedRequests.push(requestId);
}
}
// If there are no pending requests, automatically fetch the next
// unfetched chunk of the PDF
if (!this.disableAutoFetch && isEmptyObj(this.requestsByChunk)) {
var nextEmptyChunk;
if (this.stream.numChunksLoaded === 1) {
// This is a special optimization so that after fetching the first
// chunk, rather than fetching the second chunk, we fetch the last
// chunk.
var lastChunk = this.stream.numChunks - 1;
if (!this.stream.hasChunk(lastChunk)) {
nextEmptyChunk = lastChunk;
}
} else {
nextEmptyChunk = this.stream.nextEmptyChunk(endChunk);
}
if (isInt(nextEmptyChunk)) {
this.requestChunks([nextEmptyChunk]);
}
}
for (i = 0; i < loadedRequests.length; ++i) {
requestId = loadedRequests[i];
var callback = this.callbacksByRequest[requestId];
delete this.callbacksByRequest[requestId];
if (callback) {
callback();
}
}
this.msgHandler.send('DocProgress', {
loaded: this.stream.numChunksLoaded * this.chunkSize,
total: this.length
});
},
onError: function ChunkedStreamManager_onError(err) {
this._loadedStreamCapability.reject(err);
},
getBeginChunk: function ChunkedStreamManager_getBeginChunk(begin) {
var chunk = Math.floor(begin / this.chunkSize);
return chunk;
},
getEndChunk: function ChunkedStreamManager_getEndChunk(end) {
if (end % this.chunkSize === 0) {
return end / this.chunkSize;
}
// 0 -> 0
// 1 -> 1
// 99 -> 1
// 100 -> 1
// 101 -> 2
var chunk = Math.floor((end - 1) / this.chunkSize) + 1;
return chunk;
}
};
return ChunkedStreamManager;
})();
@@ -0,0 +1,956 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals Util, isString, isInt, warn, error, isCmd, isEOF, isName, Lexer,
isStream, StringStream, PDFJS, assert */
'use strict';
var BUILT_IN_CMAPS = [
// << Start unicode maps.
'Adobe-GB1-UCS2',
'Adobe-CNS1-UCS2',
'Adobe-Japan1-UCS2',
'Adobe-Korea1-UCS2',
// >> End unicode maps.
'78-EUC-H',
'78-EUC-V',
'78-H',
'78-RKSJ-H',
'78-RKSJ-V',
'78-V',
'78ms-RKSJ-H',
'78ms-RKSJ-V',
'83pv-RKSJ-H',
'90ms-RKSJ-H',
'90ms-RKSJ-V',
'90msp-RKSJ-H',
'90msp-RKSJ-V',
'90pv-RKSJ-H',
'90pv-RKSJ-V',
'Add-H',
'Add-RKSJ-H',
'Add-RKSJ-V',
'Add-V',
'Adobe-CNS1-0',
'Adobe-CNS1-1',
'Adobe-CNS1-2',
'Adobe-CNS1-3',
'Adobe-CNS1-4',
'Adobe-CNS1-5',
'Adobe-CNS1-6',
'Adobe-GB1-0',
'Adobe-GB1-1',
'Adobe-GB1-2',
'Adobe-GB1-3',
'Adobe-GB1-4',
'Adobe-GB1-5',
'Adobe-Japan1-0',
'Adobe-Japan1-1',
'Adobe-Japan1-2',
'Adobe-Japan1-3',
'Adobe-Japan1-4',
'Adobe-Japan1-5',
'Adobe-Japan1-6',
'Adobe-Korea1-0',
'Adobe-Korea1-1',
'Adobe-Korea1-2',
'B5-H',
'B5-V',
'B5pc-H',
'B5pc-V',
'CNS-EUC-H',
'CNS-EUC-V',
'CNS1-H',
'CNS1-V',
'CNS2-H',
'CNS2-V',
'ETHK-B5-H',
'ETHK-B5-V',
'ETen-B5-H',
'ETen-B5-V',
'ETenms-B5-H',
'ETenms-B5-V',
'EUC-H',
'EUC-V',
'Ext-H',
'Ext-RKSJ-H',
'Ext-RKSJ-V',
'Ext-V',
'GB-EUC-H',
'GB-EUC-V',
'GB-H',
'GB-V',
'GBK-EUC-H',
'GBK-EUC-V',
'GBK2K-H',
'GBK2K-V',
'GBKp-EUC-H',
'GBKp-EUC-V',
'GBT-EUC-H',
'GBT-EUC-V',
'GBT-H',
'GBT-V',
'GBTpc-EUC-H',
'GBTpc-EUC-V',
'GBpc-EUC-H',
'GBpc-EUC-V',
'H',
'HKdla-B5-H',
'HKdla-B5-V',
'HKdlb-B5-H',
'HKdlb-B5-V',
'HKgccs-B5-H',
'HKgccs-B5-V',
'HKm314-B5-H',
'HKm314-B5-V',
'HKm471-B5-H',
'HKm471-B5-V',
'HKscs-B5-H',
'HKscs-B5-V',
'Hankaku',
'Hiragana',
'KSC-EUC-H',
'KSC-EUC-V',
'KSC-H',
'KSC-Johab-H',
'KSC-Johab-V',
'KSC-V',
'KSCms-UHC-H',
'KSCms-UHC-HW-H',
'KSCms-UHC-HW-V',
'KSCms-UHC-V',
'KSCpc-EUC-H',
'KSCpc-EUC-V',
'Katakana',
'NWP-H',
'NWP-V',
'RKSJ-H',
'RKSJ-V',
'Roman',
'UniCNS-UCS2-H',
'UniCNS-UCS2-V',
'UniCNS-UTF16-H',
'UniCNS-UTF16-V',
'UniCNS-UTF32-H',
'UniCNS-UTF32-V',
'UniCNS-UTF8-H',
'UniCNS-UTF8-V',
'UniGB-UCS2-H',
'UniGB-UCS2-V',
'UniGB-UTF16-H',
'UniGB-UTF16-V',
'UniGB-UTF32-H',
'UniGB-UTF32-V',
'UniGB-UTF8-H',
'UniGB-UTF8-V',
'UniJIS-UCS2-H',
'UniJIS-UCS2-HW-H',
'UniJIS-UCS2-HW-V',
'UniJIS-UCS2-V',
'UniJIS-UTF16-H',
'UniJIS-UTF16-V',
'UniJIS-UTF32-H',
'UniJIS-UTF32-V',
'UniJIS-UTF8-H',
'UniJIS-UTF8-V',
'UniJIS2004-UTF16-H',
'UniJIS2004-UTF16-V',
'UniJIS2004-UTF32-H',
'UniJIS2004-UTF32-V',
'UniJIS2004-UTF8-H',
'UniJIS2004-UTF8-V',
'UniJISPro-UCS2-HW-V',
'UniJISPro-UCS2-V',
'UniJISPro-UTF8-V',
'UniJISX0213-UTF32-H',
'UniJISX0213-UTF32-V',
'UniJISX02132004-UTF32-H',
'UniJISX02132004-UTF32-V',
'UniKS-UCS2-H',
'UniKS-UCS2-V',
'UniKS-UTF16-H',
'UniKS-UTF16-V',
'UniKS-UTF32-H',
'UniKS-UTF32-V',
'UniKS-UTF8-H',
'UniKS-UTF8-V',
'V',
'WP-Symbol'];
// CMap, not to be confused with TrueType's cmap.
var CMap = (function CMapClosure() {
function CMap(builtInCMap) {
// Codespace ranges are stored as follows:
// [[1BytePairs], [2BytePairs], [3BytePairs], [4BytePairs]]
// where nBytePairs are ranges e.g. [low1, high1, low2, high2, ...]
this.codespaceRanges = [[], [], [], []];
this.numCodespaceRanges = 0;
// Map entries have one of two forms.
// - cid chars are 16-bit unsigned integers, stored as integers.
// - bf chars are variable-length byte sequences, stored as strings, with
// one byte per character.
this._map = [];
this.vertical = false;
this.useCMap = null;
this.builtInCMap = builtInCMap;
}
CMap.prototype = {
addCodespaceRange: function(n, low, high) {
this.codespaceRanges[n - 1].push(low, high);
this.numCodespaceRanges++;
},
mapCidRange: function(low, high, dstLow) {
while (low <= high) {
this._map[low++] = dstLow++;
}
},
mapBfRange: function(low, high, dstLow) {
var lastByte = dstLow.length - 1;
while (low <= high) {
this._map[low++] = dstLow;
// Only the last byte has to be incremented.
dstLow = dstLow.substr(0, lastByte) +
String.fromCharCode(dstLow.charCodeAt(lastByte) + 1);
}
},
mapBfRangeToArray: function(low, high, array) {
var i = 0, ii = array.length;
while (low <= high && i < ii) {
this._map[low] = array[i++];
++low;
}
},
// This is used for both bf and cid chars.
mapOne: function(src, dst) {
this._map[src] = dst;
},
lookup: function(code) {
return this._map[code];
},
contains: function(code) {
return this._map[code] !== undefined;
},
forEach: function(callback) {
// Most maps have fewer than 65536 entries, and for those we use normal
// array iteration. But really sparse tables are possible -- e.g. with
// indices in the *billions*. For such tables we use for..in, which isn't
// ideal because it stringifies the indices for all present elements, but
// it does avoid iterating over every undefined entry.
var map = this._map;
var length = map.length;
var i;
if (length <= 0x10000) {
for (i = 0; i < length; i++) {
if (map[i] !== undefined) {
callback(i, map[i]);
}
}
} else {
for (i in this._map) {
callback(i, map[i]);
}
}
},
charCodeOf: function(value) {
return this._map.indexOf(value);
},
getMap: function() {
return this._map;
},
readCharCode: function(str, offset, out) {
var c = 0;
var codespaceRanges = this.codespaceRanges;
var codespaceRangesLen = this.codespaceRanges.length;
// 9.7.6.2 CMap Mapping
// The code length is at most 4.
for (var n = 0; n < codespaceRangesLen; n++) {
c = ((c << 8) | str.charCodeAt(offset + n)) >>> 0;
// Check each codespace range to see if it falls within.
var codespaceRange = codespaceRanges[n];
for (var k = 0, kk = codespaceRange.length; k < kk;) {
var low = codespaceRange[k++];
var high = codespaceRange[k++];
if (c >= low && c <= high) {
out.charcode = c;
out.length = n + 1;
return;
}
}
}
out.charcode = 0;
out.length = 1;
}
};
return CMap;
})();
// A special case of CMap, where the _map array implicitly has a length of
// 65535 and each element is equal to its index.
var IdentityCMap = (function IdentityCMapClosure() {
function IdentityCMap(vertical, n) {
CMap.call(this);
this.vertical = vertical;
this.addCodespaceRange(n, 0, 0xffff);
}
Util.inherit(IdentityCMap, CMap, {});
IdentityCMap.prototype = {
addCodespaceRange: CMap.prototype.addCodespaceRange,
mapCidRange: function(low, high, dstLow) {
error('should not call mapCidRange');
},
mapBfRange: function(low, high, dstLow) {
error('should not call mapBfRange');
},
mapBfRangeToArray: function(low, high, array) {
error('should not call mapBfRangeToArray');
},
mapOne: function(src, dst) {
error('should not call mapCidOne');
},
lookup: function(code) {
return (isInt(code) && code <= 0xffff) ? code : undefined;
},
contains: function(code) {
return isInt(code) && code <= 0xffff;
},
forEach: function(callback) {
for (var i = 0; i <= 0xffff; i++) {
callback(i, i);
}
},
charCodeOf: function(value) {
return (isInt(value) && value <= 0xffff) ? value : -1;
},
getMap: function() {
// Sometimes identity maps must be instantiated, but it's rare.
var map = new Array(0x10000);
for (var i = 0; i <= 0xffff; i++) {
map[i] = i;
}
return map;
},
readCharCode: CMap.prototype.readCharCode
};
return IdentityCMap;
})();
var BinaryCMapReader = (function BinaryCMapReaderClosure() {
function fetchBinaryData(url) {
var nonBinaryRequest = PDFJS.disableWorker;
var request = new XMLHttpRequest();
request.open('GET', url, false);
if (!nonBinaryRequest) {
try {
request.responseType = 'arraybuffer';
nonBinaryRequest = request.responseType !== 'arraybuffer';
} catch (e) {
nonBinaryRequest = true;
}
}
if (nonBinaryRequest && request.overrideMimeType) {
request.overrideMimeType('text/plain; charset=x-user-defined');
}
request.send(null);
if (nonBinaryRequest ? !request.responseText : !request.response) {
error('Unable to get binary cMap at: ' + url);
}
if (nonBinaryRequest) {
var data = Array.prototype.map.call(request.responseText, function (ch) {
return ch.charCodeAt(0) & 255;
});
return new Uint8Array(data);
}
return new Uint8Array(request.response);
}
function hexToInt(a, size) {
var n = 0;
for (var i = 0; i <= size; i++) {
n = (n << 8) | a[i];
}
return n >>> 0;
}
function hexToStr(a, size) {
// This code is hot. Special-case some common values to avoid creating an
// object with subarray().
if (size === 1) {
return String.fromCharCode(a[0], a[1]);
}
if (size === 3) {
return String.fromCharCode(a[0], a[1], a[2], a[3]);
}
return String.fromCharCode.apply(null, a.subarray(0, size + 1));
}
function addHex(a, b, size) {
var c = 0;
for (var i = size; i >= 0; i--) {
c += a[i] + b[i];
a[i] = c & 255;
c >>= 8;
}
}
function incHex(a, size) {
var c = 1;
for (var i = size; i >= 0 && c > 0; i--) {
c += a[i];
a[i] = c & 255;
c >>= 8;
}
}
var MAX_NUM_SIZE = 16;
var MAX_ENCODED_NUM_SIZE = 19; // ceil(MAX_NUM_SIZE * 7 / 8)
function BinaryCMapStream(data) {
this.buffer = data;
this.pos = 0;
this.end = data.length;
this.tmpBuf = new Uint8Array(MAX_ENCODED_NUM_SIZE);
}
BinaryCMapStream.prototype = {
readByte: function () {
if (this.pos >= this.end) {
return -1;
}
return this.buffer[this.pos++];
},
readNumber: function () {
var n = 0;
var last;
do {
var b = this.readByte();
if (b < 0) {
error('unexpected EOF in bcmap');
}
last = !(b & 0x80);
n = (n << 7) | (b & 0x7F);
} while (!last);
return n;
},
readSigned: function () {
var n = this.readNumber();
return (n & 1) ? ~(n >>> 1) : n >>> 1;
},
readHex: function (num, size) {
num.set(this.buffer.subarray(this.pos,
this.pos + size + 1));
this.pos += size + 1;
},
readHexNumber: function (num, size) {
var last;
var stack = this.tmpBuf, sp = 0;
do {
var b = this.readByte();
if (b < 0) {
error('unexpected EOF in bcmap');
}
last = !(b & 0x80);
stack[sp++] = b & 0x7F;
} while (!last);
var i = size, buffer = 0, bufferSize = 0;
while (i >= 0) {
while (bufferSize < 8 && stack.length > 0) {
buffer = (stack[--sp] << bufferSize) | buffer;
bufferSize += 7;
}
num[i] = buffer & 255;
i--;
buffer >>= 8;
bufferSize -= 8;
}
},
readHexSigned: function (num, size) {
this.readHexNumber(num, size);
var sign = num[size] & 1 ? 255 : 0;
var c = 0;
for (var i = 0; i <= size; i++) {
c = ((c & 1) << 8) | num[i];
num[i] = (c >> 1) ^ sign;
}
},
readString: function () {
var len = this.readNumber();
var s = '';
for (var i = 0; i < len; i++) {
s += String.fromCharCode(this.readNumber());
}
return s;
}
};
function processBinaryCMap(url, cMap, extend) {
var data = fetchBinaryData(url);
var stream = new BinaryCMapStream(data);
var header = stream.readByte();
cMap.vertical = !!(header & 1);
var useCMap = null;
var start = new Uint8Array(MAX_NUM_SIZE);
var end = new Uint8Array(MAX_NUM_SIZE);
var char = new Uint8Array(MAX_NUM_SIZE);
var charCode = new Uint8Array(MAX_NUM_SIZE);
var tmp = new Uint8Array(MAX_NUM_SIZE);
var code;
var b;
while ((b = stream.readByte()) >= 0) {
var type = b >> 5;
if (type === 7) { // metadata, e.g. comment or usecmap
switch (b & 0x1F) {
case 0:
stream.readString(); // skipping comment
break;
case 1:
useCMap = stream.readString();
break;
}
continue;
}
var sequence = !!(b & 0x10);
var dataSize = b & 15;
assert(dataSize + 1 <= MAX_NUM_SIZE);
var ucs2DataSize = 1;
var subitemsCount = stream.readNumber();
var i;
switch (type) {
case 0: // codespacerange
stream.readHex(start, dataSize);
stream.readHexNumber(end, dataSize);
addHex(end, start, dataSize);
cMap.addCodespaceRange(dataSize + 1, hexToInt(start, dataSize),
hexToInt(end, dataSize));
for (i = 1; i < subitemsCount; i++) {
incHex(end, dataSize);
stream.readHexNumber(start, dataSize);
addHex(start, end, dataSize);
stream.readHexNumber(end, dataSize);
addHex(end, start, dataSize);
cMap.addCodespaceRange(dataSize + 1, hexToInt(start, dataSize),
hexToInt(end, dataSize));
}
break;
case 1: // notdefrange
stream.readHex(start, dataSize);
stream.readHexNumber(end, dataSize);
addHex(end, start, dataSize);
code = stream.readNumber();
// undefined range, skipping
for (i = 1; i < subitemsCount; i++) {
incHex(end, dataSize);
stream.readHexNumber(start, dataSize);
addHex(start, end, dataSize);
stream.readHexNumber(end, dataSize);
addHex(end, start, dataSize);
code = stream.readNumber();
// nop
}
break;
case 2: // cidchar
stream.readHex(char, dataSize);
code = stream.readNumber();
cMap.mapOne(hexToInt(char, dataSize), code);
for (i = 1; i < subitemsCount; i++) {
incHex(char, dataSize);
if (!sequence) {
stream.readHexNumber(tmp, dataSize);
addHex(char, tmp, dataSize);
}
code = stream.readSigned() + (code + 1);
cMap.mapOne(hexToInt(char, dataSize), code);
}
break;
case 3: // cidrange
stream.readHex(start, dataSize);
stream.readHexNumber(end, dataSize);
addHex(end, start, dataSize);
code = stream.readNumber();
cMap.mapCidRange(hexToInt(start, dataSize), hexToInt(end, dataSize),
code);
for (i = 1; i < subitemsCount; i++) {
incHex(end, dataSize);
if (!sequence) {
stream.readHexNumber(start, dataSize);
addHex(start, end, dataSize);
} else {
start.set(end);
}
stream.readHexNumber(end, dataSize);
addHex(end, start, dataSize);
code = stream.readNumber();
cMap.mapCidRange(hexToInt(start, dataSize), hexToInt(end, dataSize),
code);
}
break;
case 4: // bfchar
stream.readHex(char, ucs2DataSize);
stream.readHex(charCode, dataSize);
cMap.mapOne(hexToInt(char, ucs2DataSize),
hexToStr(charCode, dataSize));
for (i = 1; i < subitemsCount; i++) {
incHex(char, ucs2DataSize);
if (!sequence) {
stream.readHexNumber(tmp, ucs2DataSize);
addHex(char, tmp, ucs2DataSize);
}
incHex(charCode, dataSize);
stream.readHexSigned(tmp, dataSize);
addHex(charCode, tmp, dataSize);
cMap.mapOne(hexToInt(char, ucs2DataSize),
hexToStr(charCode, dataSize));
}
break;
case 5: // bfrange
stream.readHex(start, ucs2DataSize);
stream.readHexNumber(end, ucs2DataSize);
addHex(end, start, ucs2DataSize);
stream.readHex(charCode, dataSize);
cMap.mapBfRange(hexToInt(start, ucs2DataSize),
hexToInt(end, ucs2DataSize),
hexToStr(charCode, dataSize));
for (i = 1; i < subitemsCount; i++) {
incHex(end, ucs2DataSize);
if (!sequence) {
stream.readHexNumber(start, ucs2DataSize);
addHex(start, end, ucs2DataSize);
} else {
start.set(end);
}
stream.readHexNumber(end, ucs2DataSize);
addHex(end, start, ucs2DataSize);
stream.readHex(charCode, dataSize);
cMap.mapBfRange(hexToInt(start, ucs2DataSize),
hexToInt(end, ucs2DataSize),
hexToStr(charCode, dataSize));
}
break;
default:
error('Unknown type: ' + type);
break;
}
}
if (useCMap) {
extend(useCMap);
}
return cMap;
}
function BinaryCMapReader() {}
BinaryCMapReader.prototype = {
read: processBinaryCMap
};
return BinaryCMapReader;
})();
var CMapFactory = (function CMapFactoryClosure() {
function strToInt(str) {
var a = 0;
for (var i = 0; i < str.length; i++) {
a = (a << 8) | str.charCodeAt(i);
}
return a >>> 0;
}
function expectString(obj) {
if (!isString(obj)) {
error('Malformed CMap: expected string.');
}
}
function expectInt(obj) {
if (!isInt(obj)) {
error('Malformed CMap: expected int.');
}
}
function parseBfChar(cMap, lexer) {
while (true) {
var obj = lexer.getObj();
if (isEOF(obj)) {
break;
}
if (isCmd(obj, 'endbfchar')) {
return;
}
expectString(obj);
var src = strToInt(obj);
obj = lexer.getObj();
// TODO are /dstName used?
expectString(obj);
var dst = obj;
cMap.mapOne(src, dst);
}
}
function parseBfRange(cMap, lexer) {
while (true) {
var obj = lexer.getObj();
if (isEOF(obj)) {
break;
}
if (isCmd(obj, 'endbfrange')) {
return;
}
expectString(obj);
var low = strToInt(obj);
obj = lexer.getObj();
expectString(obj);
var high = strToInt(obj);
obj = lexer.getObj();
if (isInt(obj) || isString(obj)) {
var dstLow = isInt(obj) ? String.fromCharCode(obj) : obj;
cMap.mapBfRange(low, high, dstLow);
} else if (isCmd(obj, '[')) {
obj = lexer.getObj();
var array = [];
while (!isCmd(obj, ']') && !isEOF(obj)) {
array.push(obj);
obj = lexer.getObj();
}
cMap.mapBfRangeToArray(low, high, array);
} else {
break;
}
}
error('Invalid bf range.');
}
function parseCidChar(cMap, lexer) {
while (true) {
var obj = lexer.getObj();
if (isEOF(obj)) {
break;
}
if (isCmd(obj, 'endcidchar')) {
return;
}
expectString(obj);
var src = strToInt(obj);
obj = lexer.getObj();
expectInt(obj);
var dst = obj;
cMap.mapOne(src, dst);
}
}
function parseCidRange(cMap, lexer) {
while (true) {
var obj = lexer.getObj();
if (isEOF(obj)) {
break;
}
if (isCmd(obj, 'endcidrange')) {
return;
}
expectString(obj);
var low = strToInt(obj);
obj = lexer.getObj();
expectString(obj);
var high = strToInt(obj);
obj = lexer.getObj();
expectInt(obj);
var dstLow = obj;
cMap.mapCidRange(low, high, dstLow);
}
}
function parseCodespaceRange(cMap, lexer) {
while (true) {
var obj = lexer.getObj();
if (isEOF(obj)) {
break;
}
if (isCmd(obj, 'endcodespacerange')) {
return;
}
if (!isString(obj)) {
break;
}
var low = strToInt(obj);
obj = lexer.getObj();
if (!isString(obj)) {
break;
}
var high = strToInt(obj);
cMap.addCodespaceRange(obj.length, low, high);
}
error('Invalid codespace range.');
}
function parseWMode(cMap, lexer) {
var obj = lexer.getObj();
if (isInt(obj)) {
cMap.vertical = !!obj;
}
}
function parseCMap(cMap, lexer, builtInCMapParams, useCMap) {
var previous;
var embededUseCMap;
objLoop: while (true) {
var obj = lexer.getObj();
if (isEOF(obj)) {
break;
} else if (isName(obj)) {
if (obj.name === 'WMode') {
parseWMode(cMap, lexer);
}
previous = obj;
} else if (isCmd(obj)) {
switch (obj.cmd) {
case 'endcmap':
break objLoop;
case 'usecmap':
if (isName(previous)) {
embededUseCMap = previous.name;
}
break;
case 'begincodespacerange':
parseCodespaceRange(cMap, lexer);
break;
case 'beginbfchar':
parseBfChar(cMap, lexer);
break;
case 'begincidchar':
parseCidChar(cMap, lexer);
break;
case 'beginbfrange':
parseBfRange(cMap, lexer);
break;
case 'begincidrange':
parseCidRange(cMap, lexer);
break;
}
}
}
if (!useCMap && embededUseCMap) {
// Load the usecmap definition from the file only if there wasn't one
// specified.
useCMap = embededUseCMap;
}
if (useCMap) {
extendCMap(cMap, builtInCMapParams, useCMap);
}
}
function extendCMap(cMap, builtInCMapParams, useCMap) {
cMap.useCMap = createBuiltInCMap(useCMap, builtInCMapParams);
// If there aren't any code space ranges defined clone all the parent ones
// into this cMap.
if (cMap.numCodespaceRanges === 0) {
var useCodespaceRanges = cMap.useCMap.codespaceRanges;
for (var i = 0; i < useCodespaceRanges.length; i++) {
cMap.codespaceRanges[i] = useCodespaceRanges[i].slice();
}
cMap.numCodespaceRanges = cMap.useCMap.numCodespaceRanges;
}
// Merge the map into the current one, making sure not to override
// any previously defined entries.
cMap.useCMap.forEach(function(key, value) {
if (!cMap.contains(key)) {
cMap.mapOne(key, cMap.useCMap.lookup(key));
}
});
}
function parseBinaryCMap(name, builtInCMapParams) {
var url = builtInCMapParams.url + name + '.bcmap';
var cMap = new CMap(true);
new BinaryCMapReader().read(url, cMap, function (useCMap) {
extendCMap(cMap, builtInCMapParams, useCMap);
});
return cMap;
}
function createBuiltInCMap(name, builtInCMapParams) {
if (name === 'Identity-H') {
return new IdentityCMap(false, 2);
} else if (name === 'Identity-V') {
return new IdentityCMap(true, 2);
}
if (BUILT_IN_CMAPS.indexOf(name) === -1) {
error('Unknown cMap name: ' + name);
}
assert(builtInCMapParams, 'built-in cMap parameters are not provided');
if (builtInCMapParams.packed) {
return parseBinaryCMap(name, builtInCMapParams);
}
var request = new XMLHttpRequest();
var url = builtInCMapParams.url + name;
request.open('GET', url, false);
request.send(null);
if (!request.responseText) {
error('Unable to get cMap at: ' + url);
}
var cMap = new CMap(true);
var lexer = new Lexer(new StringStream(request.responseText));
parseCMap(cMap, lexer, builtInCMapParams, null);
return cMap;
}
return {
create: function (encoding, builtInCMapParams, useCMap) {
if (isName(encoding)) {
return createBuiltInCMap(encoding.name, builtInCMapParams);
} else if (isStream(encoding)) {
var cMap = new CMap();
var lexer = new Lexer(encoding);
try {
parseCMap(cMap, lexer, builtInCMapParams, useCMap);
} catch (e) {
warn('Invalid CMap data. ' + e);
}
return cMap;
}
error('Encoding required.');
}
};
})();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,533 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals assert, calculateMD5, Catalog, Dict, error, info, isArray,
isArrayBuffer, isName, isStream, isString, createPromiseCapability,
Linearization, NullStream, PartialEvaluator, shadow, Stream, Lexer,
StreamsSequenceStream, stringToPDFString, stringToBytes, Util, XRef,
MissingDataException, Promise, Annotation, ObjectLoader, OperatorList
*/
'use strict';
var Page = (function PageClosure() {
var LETTER_SIZE_MEDIABOX = [0, 0, 612, 792];
function Page(pdfManager, xref, pageIndex, pageDict, ref, fontCache) {
this.pdfManager = pdfManager;
this.pageIndex = pageIndex;
this.pageDict = pageDict;
this.xref = xref;
this.ref = ref;
this.fontCache = fontCache;
this.idCounters = {
obj: 0
};
this.resourcesPromise = null;
}
Page.prototype = {
getPageProp: function Page_getPageProp(key) {
return this.pageDict.get(key);
},
getInheritedPageProp: function Page_inheritPageProp(key) {
var dict = this.pageDict;
var value = dict.get(key);
while (value === undefined) {
dict = dict.get('Parent');
if (!dict) {
break;
}
value = dict.get(key);
}
return value;
},
get content() {
return this.getPageProp('Contents');
},
get resources() {
var value = this.getInheritedPageProp('Resources');
// For robustness: The spec states that a \Resources entry has to be
// present, but can be empty. Some document omit it still. In this case
// return an empty dictionary:
if (value === undefined) {
value = Dict.empty;
}
return shadow(this, 'resources', value);
},
get mediaBox() {
var obj = this.getInheritedPageProp('MediaBox');
// Reset invalid media box to letter size.
if (!isArray(obj) || obj.length !== 4) {
obj = LETTER_SIZE_MEDIABOX;
}
return shadow(this, 'mediaBox', obj);
},
get view() {
var mediaBox = this.mediaBox;
var cropBox = this.getInheritedPageProp('CropBox');
if (!isArray(cropBox) || cropBox.length !== 4) {
return shadow(this, 'view', mediaBox);
}
// From the spec, 6th ed., p.963:
// "The crop, bleed, trim, and art boxes should not ordinarily
// extend beyond the boundaries of the media box. If they do, they are
// effectively reduced to their intersection with the media box."
cropBox = Util.intersect(cropBox, mediaBox);
if (!cropBox) {
return shadow(this, 'view', mediaBox);
}
return shadow(this, 'view', cropBox);
},
get annotationRefs() {
return shadow(this, 'annotationRefs',
this.getInheritedPageProp('Annots'));
},
get rotate() {
var rotate = this.getInheritedPageProp('Rotate') || 0;
// Normalize rotation so it's a multiple of 90 and between 0 and 270
if (rotate % 90 !== 0) {
rotate = 0;
} else if (rotate >= 360) {
rotate = rotate % 360;
} else if (rotate < 0) {
// The spec doesn't cover negatives, assume its counterclockwise
// rotation. The following is the other implementation of modulo.
rotate = ((rotate % 360) + 360) % 360;
}
return shadow(this, 'rotate', rotate);
},
getContentStream: function Page_getContentStream() {
var content = this.content;
var stream;
if (isArray(content)) {
// fetching items
var xref = this.xref;
var i, n = content.length;
var streams = [];
for (i = 0; i < n; ++i) {
streams.push(xref.fetchIfRef(content[i]));
}
stream = new StreamsSequenceStream(streams);
} else if (isStream(content)) {
stream = content;
} else {
// replacing non-existent page content with empty one
stream = new NullStream();
}
return stream;
},
loadResources: function Page_loadResources(keys) {
if (!this.resourcesPromise) {
// TODO: add async getInheritedPageProp and remove this.
this.resourcesPromise = this.pdfManager.ensure(this, 'resources');
}
return this.resourcesPromise.then(function resourceSuccess() {
var objectLoader = new ObjectLoader(this.resources.map,
keys,
this.xref);
return objectLoader.load();
}.bind(this));
},
getOperatorList: function Page_getOperatorList(handler, intent) {
var self = this;
var pdfManager = this.pdfManager;
var contentStreamPromise = pdfManager.ensure(this, 'getContentStream',
[]);
var resourcesPromise = this.loadResources([
'ExtGState',
'ColorSpace',
'Pattern',
'Shading',
'XObject',
'Font'
// ProcSet
// Properties
]);
var partialEvaluator = new PartialEvaluator(pdfManager, this.xref,
handler, this.pageIndex,
'p' + this.pageIndex + '_',
this.idCounters,
this.fontCache);
var dataPromises = Promise.all([contentStreamPromise, resourcesPromise]);
var pageListPromise = dataPromises.then(function(data) {
var contentStream = data[0];
var opList = new OperatorList(intent, handler, self.pageIndex);
handler.send('StartRenderPage', {
transparency: partialEvaluator.hasBlendModes(self.resources),
pageIndex: self.pageIndex,
intent: intent
});
return partialEvaluator.getOperatorList(contentStream, self.resources,
opList).then(function () {
return opList;
});
});
var annotationsPromise = pdfManager.ensure(this, 'annotations');
return Promise.all([pageListPromise, annotationsPromise]).then(
function(datas) {
var pageOpList = datas[0];
var annotations = datas[1];
if (annotations.length === 0) {
pageOpList.flush(true);
return pageOpList;
}
var annotationsReadyPromise = Annotation.appendToOperatorList(
annotations, pageOpList, pdfManager, partialEvaluator, intent);
return annotationsReadyPromise.then(function () {
pageOpList.flush(true);
return pageOpList;
});
});
},
extractTextContent: function Page_extractTextContent() {
var handler = {
on: function nullHandlerOn() {},
send: function nullHandlerSend() {}
};
var self = this;
var pdfManager = this.pdfManager;
var contentStreamPromise = pdfManager.ensure(this, 'getContentStream',
[]);
var resourcesPromise = this.loadResources([
'ExtGState',
'XObject',
'Font'
]);
var dataPromises = Promise.all([contentStreamPromise,
resourcesPromise]);
return dataPromises.then(function(data) {
var contentStream = data[0];
var partialEvaluator = new PartialEvaluator(pdfManager, self.xref,
handler, self.pageIndex,
'p' + self.pageIndex + '_',
self.idCounters,
self.fontCache);
return partialEvaluator.getTextContent(contentStream,
self.resources);
});
},
getAnnotationsData: function Page_getAnnotationsData() {
var annotations = this.annotations;
var annotationsData = [];
for (var i = 0, n = annotations.length; i < n; ++i) {
annotationsData.push(annotations[i].getData());
}
return annotationsData;
},
get annotations() {
var annotations = [];
var annotationRefs = (this.annotationRefs || []);
for (var i = 0, n = annotationRefs.length; i < n; ++i) {
var annotationRef = annotationRefs[i];
var annotation = Annotation.fromRef(this.xref, annotationRef);
if (annotation) {
annotations.push(annotation);
}
}
return shadow(this, 'annotations', annotations);
}
};
return Page;
})();
/**
* The `PDFDocument` holds all the data of the PDF file. Compared to the
* `PDFDoc`, this one doesn't have any job management code.
* Right now there exists one PDFDocument on the main thread + one object
* for each worker. If there is no worker support enabled, there are two
* `PDFDocument` objects on the main thread created.
*/
var PDFDocument = (function PDFDocumentClosure() {
var FINGERPRINT_FIRST_BYTES = 1024;
var EMPTY_FINGERPRINT = '\x00\x00\x00\x00\x00\x00\x00' +
'\x00\x00\x00\x00\x00\x00\x00\x00\x00';
function PDFDocument(pdfManager, arg, password) {
if (isStream(arg)) {
init.call(this, pdfManager, arg, password);
} else if (isArrayBuffer(arg)) {
init.call(this, pdfManager, new Stream(arg), password);
} else {
error('PDFDocument: Unknown argument type');
}
}
function init(pdfManager, stream, password) {
assert(stream.length > 0, 'stream must have data');
this.pdfManager = pdfManager;
this.stream = stream;
var xref = new XRef(this.stream, password, pdfManager);
this.xref = xref;
}
function find(stream, needle, limit, backwards) {
var pos = stream.pos;
var end = stream.end;
var strBuf = [];
if (pos + limit > end) {
limit = end - pos;
}
for (var n = 0; n < limit; ++n) {
strBuf.push(String.fromCharCode(stream.getByte()));
}
var str = strBuf.join('');
stream.pos = pos;
var index = backwards ? str.lastIndexOf(needle) : str.indexOf(needle);
if (index === -1) {
return false; /* not found */
}
stream.pos += index;
return true; /* found */
}
var DocumentInfoValidators = {
get entries() {
// Lazily build this since all the validation functions below are not
// defined until after this file loads.
return shadow(this, 'entries', {
Title: isString,
Author: isString,
Subject: isString,
Keywords: isString,
Creator: isString,
Producer: isString,
CreationDate: isString,
ModDate: isString,
Trapped: isName
});
}
};
PDFDocument.prototype = {
parse: function PDFDocument_parse(recoveryMode) {
this.setup(recoveryMode);
try {
// checking if AcroForm is present
this.acroForm = this.catalog.catDict.get('AcroForm');
if (this.acroForm) {
this.xfa = this.acroForm.get('XFA');
var fields = this.acroForm.get('Fields');
if ((!fields || !isArray(fields) || fields.length === 0) &&
!this.xfa) {
// no fields and no XFA -- not a form (?)
this.acroForm = null;
}
}
} catch (ex) {
info('Something wrong with AcroForm entry');
this.acroForm = null;
}
},
get linearization() {
var linearization = null;
if (this.stream.length) {
try {
linearization = Linearization.create(this.stream);
} catch (err) {
if (err instanceof MissingDataException) {
throw err;
}
info(err);
}
}
// shadow the prototype getter with a data property
return shadow(this, 'linearization', linearization);
},
get startXRef() {
var stream = this.stream;
var startXRef = 0;
var linearization = this.linearization;
if (linearization) {
// Find end of first obj.
stream.reset();
if (find(stream, 'endobj', 1024)) {
startXRef = stream.pos + 6;
}
} else {
// Find startxref by jumping backward from the end of the file.
var step = 1024;
var found = false, pos = stream.end;
while (!found && pos > 0) {
pos -= step - 'startxref'.length;
if (pos < 0) {
pos = 0;
}
stream.pos = pos;
found = find(stream, 'startxref', step, true);
}
if (found) {
stream.skip(9);
var ch;
do {
ch = stream.getByte();
} while (Lexer.isSpace(ch));
var str = '';
while (ch >= 0x20 && ch <= 0x39) { // < '9'
str += String.fromCharCode(ch);
ch = stream.getByte();
}
startXRef = parseInt(str, 10);
if (isNaN(startXRef)) {
startXRef = 0;
}
}
}
// shadow the prototype getter with a data property
return shadow(this, 'startXRef', startXRef);
},
get mainXRefEntriesOffset() {
var mainXRefEntriesOffset = 0;
var linearization = this.linearization;
if (linearization) {
mainXRefEntriesOffset = linearization.mainXRefEntriesOffset;
}
// shadow the prototype getter with a data property
return shadow(this, 'mainXRefEntriesOffset', mainXRefEntriesOffset);
},
// Find the header, remove leading garbage and setup the stream
// starting from the header.
checkHeader: function PDFDocument_checkHeader() {
var stream = this.stream;
stream.reset();
if (find(stream, '%PDF-', 1024)) {
// Found the header, trim off any garbage before it.
stream.moveStart();
// Reading file format version
var MAX_VERSION_LENGTH = 12;
var version = '', ch;
while ((ch = stream.getByte()) > 0x20) { // SPACE
if (version.length >= MAX_VERSION_LENGTH) {
break;
}
version += String.fromCharCode(ch);
}
// removing "%PDF-"-prefix
this.pdfFormatVersion = version.substring(5);
return;
}
// May not be a PDF file, continue anyway.
},
parseStartXRef: function PDFDocument_parseStartXRef() {
var startXRef = this.startXRef;
this.xref.setStartXRef(startXRef);
},
setup: function PDFDocument_setup(recoveryMode) {
this.xref.parse(recoveryMode);
this.catalog = new Catalog(this.pdfManager, this.xref);
},
get numPages() {
var linearization = this.linearization;
var num = linearization ? linearization.numPages : this.catalog.numPages;
// shadow the prototype getter
return shadow(this, 'numPages', num);
},
get documentInfo() {
var docInfo = {
PDFFormatVersion: this.pdfFormatVersion,
IsAcroFormPresent: !!this.acroForm,
IsXFAPresent: !!this.xfa
};
var infoDict;
try {
infoDict = this.xref.trailer.get('Info');
} catch (err) {
info('The document information dictionary is invalid.');
}
if (infoDict) {
var validEntries = DocumentInfoValidators.entries;
// Only fill the document info with valid entries from the spec.
for (var key in validEntries) {
if (infoDict.has(key)) {
var value = infoDict.get(key);
// Make sure the value conforms to the spec.
if (validEntries[key](value)) {
docInfo[key] = (typeof value !== 'string' ?
value : stringToPDFString(value));
} else {
info('Bad value in document info for "' + key + '"');
}
}
}
}
return shadow(this, 'documentInfo', docInfo);
},
get fingerprint() {
var xref = this.xref, idArray, hash, fileID = '';
if (xref.trailer.has('ID')) {
idArray = xref.trailer.get('ID');
}
if (idArray && isArray(idArray) && idArray[0] !== EMPTY_FINGERPRINT) {
hash = stringToBytes(idArray[0]);
} else {
if (this.stream.ensureRange) {
this.stream.ensureRange(0,
Math.min(FINGERPRINT_FIRST_BYTES, this.stream.end));
}
hash = calculateMD5(this.stream.bytes.subarray(0,
FINGERPRINT_FIRST_BYTES), 0, FINGERPRINT_FIRST_BYTES);
}
for (var i = 0, n = hash.length; i < n; i++) {
var hex = hash[i].toString(16);
fileID += hex.length === 1 ? '0' + hex : hex;
}
return shadow(this, 'fingerprint', fileID);
},
getPage: function PDFDocument_getPage(pageIndex) {
return this.catalog.getPage(pageIndex);
},
cleanup: function PDFDocument_cleanup() {
return this.catalog.cleanup();
}
};
return PDFDocument;
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,713 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals error, bytesToString, Stream, GlyphsUnicode, CFFParser, Encodings,
Util */
'use strict';
var FontRendererFactory = (function FontRendererFactoryClosure() {
function getLong(data, offset) {
return (data[offset] << 24) | (data[offset + 1] << 16) |
(data[offset + 2] << 8) | data[offset + 3];
}
function getUshort(data, offset) {
return (data[offset] << 8) | data[offset + 1];
}
function parseCmap(data, start, end) {
var offset = (getUshort(data, start + 2) === 1 ?
getLong(data, start + 8) : getLong(data, start + 16));
var format = getUshort(data, start + offset);
var length, ranges, p, i;
if (format === 4) {
length = getUshort(data, start + offset + 2);
var segCount = getUshort(data, start + offset + 6) >> 1;
p = start + offset + 14;
ranges = [];
for (i = 0; i < segCount; i++, p += 2) {
ranges[i] = {end: getUshort(data, p)};
}
p += 2;
for (i = 0; i < segCount; i++, p += 2) {
ranges[i].start = getUshort(data, p);
}
for (i = 0; i < segCount; i++, p += 2) {
ranges[i].idDelta = getUshort(data, p);
}
for (i = 0; i < segCount; i++, p += 2) {
var idOffset = getUshort(data, p);
if (idOffset === 0) {
continue;
}
ranges[i].ids = [];
for (var j = 0, jj = ranges[i].end - ranges[i].start + 1; j < jj; j++) {
ranges[i].ids[j] = getUshort(data, p + idOffset);
idOffset += 2;
}
}
return ranges;
} else if (format === 12) {
length = getLong(data, start + offset + 4);
var groups = getLong(data, start + offset + 12);
p = start + offset + 16;
ranges = [];
for (i = 0; i < groups; i++) {
ranges.push({
start: getLong(data, p),
end: getLong(data, p + 4),
idDelta: getLong(data, p + 8) - getLong(data, p)
});
p += 12;
}
return ranges;
}
error('not supported cmap: ' + format);
}
function parseCff(data, start, end) {
var properties = {};
var parser = new CFFParser(new Stream(data, start, end - start),
properties);
var cff = parser.parse();
return {
glyphs: cff.charStrings.objects,
subrs: (cff.topDict.privateDict && cff.topDict.privateDict.subrsIndex &&
cff.topDict.privateDict.subrsIndex.objects),
gsubrs: cff.globalSubrIndex && cff.globalSubrIndex.objects
};
}
function parseGlyfTable(glyf, loca, isGlyphLocationsLong) {
var itemSize, itemDecode;
if (isGlyphLocationsLong) {
itemSize = 4;
itemDecode = function fontItemDecodeLong(data, offset) {
return (data[offset] << 24) | (data[offset + 1] << 16) |
(data[offset + 2] << 8) | data[offset + 3];
};
} else {
itemSize = 2;
itemDecode = function fontItemDecode(data, offset) {
return (data[offset] << 9) | (data[offset + 1] << 1);
};
}
var glyphs = [];
var startOffset = itemDecode(loca, 0);
for (var j = itemSize; j < loca.length; j += itemSize) {
var endOffset = itemDecode(loca, j);
glyphs.push(glyf.subarray(startOffset, endOffset));
startOffset = endOffset;
}
return glyphs;
}
function lookupCmap(ranges, unicode) {
var code = unicode.charCodeAt(0);
var l = 0, r = ranges.length - 1;
while (l < r) {
var c = (l + r + 1) >> 1;
if (code < ranges[c].start) {
r = c - 1;
} else {
l = c;
}
}
if (ranges[l].start <= code && code <= ranges[l].end) {
return (ranges[l].idDelta + (ranges[l].ids ?
ranges[l].ids[code - ranges[l].start] : code)) & 0xFFFF;
}
return 0;
}
function compileGlyf(code, js, font) {
function moveTo(x, y) {
js.push('c.moveTo(' + x + ',' + y + ');');
}
function lineTo(x, y) {
js.push('c.lineTo(' + x + ',' + y + ');');
}
function quadraticCurveTo(xa, ya, x, y) {
js.push('c.quadraticCurveTo(' + xa + ',' + ya + ',' +
x + ',' + y + ');');
}
var i = 0;
var numberOfContours = ((code[i] << 24) | (code[i + 1] << 16)) >> 16;
var flags;
var x = 0, y = 0;
i += 10;
if (numberOfContours < 0) {
// composite glyph
do {
flags = (code[i] << 8) | code[i + 1];
var glyphIndex = (code[i + 2] << 8) | code[i + 3];
i += 4;
var arg1, arg2;
if ((flags & 0x01)) {
arg1 = ((code[i] << 24) | (code[i + 1] << 16)) >> 16;
arg2 = ((code[i + 2] << 24) | (code[i + 3] << 16)) >> 16;
i += 4;
} else {
arg1 = code[i++]; arg2 = code[i++];
}
if ((flags & 0x02)) {
x = arg1;
y = arg2;
} else {
x = 0; y = 0; // TODO "they are points" ?
}
var scaleX = 1, scaleY = 1, scale01 = 0, scale10 = 0;
if ((flags & 0x08)) {
scaleX =
scaleY = ((code[i] << 24) | (code[i + 1] << 16)) / 1073741824;
i += 2;
} else if ((flags & 0x40)) {
scaleX = ((code[i] << 24) | (code[i + 1] << 16)) / 1073741824;
scaleY = ((code[i + 2] << 24) | (code[i + 3] << 16)) / 1073741824;
i += 4;
} else if ((flags & 0x80)) {
scaleX = ((code[i] << 24) | (code[i + 1] << 16)) / 1073741824;
scale01 = ((code[i + 2] << 24) | (code[i + 3] << 16)) / 1073741824;
scale10 = ((code[i + 4] << 24) | (code[i + 5] << 16)) / 1073741824;
scaleY = ((code[i + 6] << 24) | (code[i + 7] << 16)) / 1073741824;
i += 8;
}
var subglyph = font.glyphs[glyphIndex];
if (subglyph) {
js.push('c.save();');
js.push('c.transform(' + scaleX + ',' + scale01 + ',' +
scale10 + ',' + scaleY + ',' + x + ',' + y + ');');
compileGlyf(subglyph, js, font);
js.push('c.restore();');
}
} while ((flags & 0x20));
} else {
// simple glyph
var endPtsOfContours = [];
var j, jj;
for (j = 0; j < numberOfContours; j++) {
endPtsOfContours.push((code[i] << 8) | code[i + 1]);
i += 2;
}
var instructionLength = (code[i] << 8) | code[i + 1];
i += 2 + instructionLength; // skipping the instructions
var numberOfPoints = endPtsOfContours[endPtsOfContours.length - 1] + 1;
var points = [];
while (points.length < numberOfPoints) {
flags = code[i++];
var repeat = 1;
if ((flags & 0x08)) {
repeat += code[i++];
}
while (repeat-- > 0) {
points.push({flags: flags});
}
}
for (j = 0; j < numberOfPoints; j++) {
switch (points[j].flags & 0x12) {
case 0x00:
x += ((code[i] << 24) | (code[i + 1] << 16)) >> 16;
i += 2;
break;
case 0x02:
x -= code[i++];
break;
case 0x12:
x += code[i++];
break;
}
points[j].x = x;
}
for (j = 0; j < numberOfPoints; j++) {
switch (points[j].flags & 0x24) {
case 0x00:
y += ((code[i] << 24) | (code[i + 1] << 16)) >> 16;
i += 2;
break;
case 0x04:
y -= code[i++];
break;
case 0x24:
y += code[i++];
break;
}
points[j].y = y;
}
var startPoint = 0;
for (i = 0; i < numberOfContours; i++) {
var endPoint = endPtsOfContours[i];
// contours might have implicit points, which is located in the middle
// between two neighboring off-curve points
var contour = points.slice(startPoint, endPoint + 1);
if ((contour[0].flags & 1)) {
contour.push(contour[0]); // using start point at the contour end
} else if ((contour[contour.length - 1].flags & 1)) {
// first is off-curve point, trying to use one from the end
contour.unshift(contour[contour.length - 1]);
} else {
// start and end are off-curve points, creating implicit one
var p = {
flags: 1,
x: (contour[0].x + contour[contour.length - 1].x) / 2,
y: (contour[0].y + contour[contour.length - 1].y) / 2
};
contour.unshift(p);
contour.push(p);
}
moveTo(contour[0].x, contour[0].y);
for (j = 1, jj = contour.length; j < jj; j++) {
if ((contour[j].flags & 1)) {
lineTo(contour[j].x, contour[j].y);
} else if ((contour[j + 1].flags & 1)){
quadraticCurveTo(contour[j].x, contour[j].y,
contour[j + 1].x, contour[j + 1].y);
j++;
} else {
quadraticCurveTo(contour[j].x, contour[j].y,
(contour[j].x + contour[j + 1].x) / 2,
(contour[j].y + contour[j + 1].y) / 2);
}
}
startPoint = endPoint + 1;
}
}
}
function compileCharString(code, js, font) {
var stack = [];
var x = 0, y = 0;
var stems = 0;
function moveTo(x, y) {
js.push('c.moveTo(' + x + ',' + y + ');');
}
function lineTo(x, y) {
js.push('c.lineTo(' + x + ',' + y + ');');
}
function bezierCurveTo(x1, y1, x2, y2, x, y) {
js.push('c.bezierCurveTo(' + x1 + ',' + y1 + ',' + x2 + ',' + y2 + ',' +
x + ',' + y + ');');
}
function parse(code) {
var i = 0;
while (i < code.length) {
var stackClean = false;
var v = code[i++];
var xa, xb, ya, yb, y1, y2, y3, n, subrCode;
switch (v) {
case 1: // hstem
stems += stack.length >> 1;
stackClean = true;
break;
case 3: // vstem
stems += stack.length >> 1;
stackClean = true;
break;
case 4: // vmoveto
y += stack.pop();
moveTo(x, y);
stackClean = true;
break;
case 5: // rlineto
while (stack.length > 0) {
x += stack.shift();
y += stack.shift();
lineTo(x, y);
}
break;
case 6: // hlineto
while (stack.length > 0) {
x += stack.shift();
lineTo(x, y);
if (stack.length === 0) {
break;
}
y += stack.shift();
lineTo(x, y);
}
break;
case 7: // vlineto
while (stack.length > 0) {
y += stack.shift();
lineTo(x, y);
if (stack.length === 0) {
break;
}
x += stack.shift();
lineTo(x, y);
}
break;
case 8: // rrcurveto
while (stack.length > 0) {
xa = x + stack.shift(); ya = y + stack.shift();
xb = xa + stack.shift(); yb = ya + stack.shift();
x = xb + stack.shift(); y = yb + stack.shift();
bezierCurveTo(xa, ya, xb, yb, x, y);
}
break;
case 10: // callsubr
n = stack.pop() + font.subrsBias;
subrCode = font.subrs[n];
if (subrCode) {
parse(subrCode);
}
break;
case 11: // return
return;
case 12:
v = code[i++];
switch (v) {
case 34: // flex
xa = x + stack.shift();
xb = xa + stack.shift(); y1 = y + stack.shift();
x = xb + stack.shift();
bezierCurveTo(xa, y, xb, y1, x, y1);
xa = x + stack.shift();
xb = xa + stack.shift();
x = xb + stack.shift();
bezierCurveTo(xa, y1, xb, y, x, y);
break;
case 35: // flex
xa = x + stack.shift(); ya = y + stack.shift();
xb = xa + stack.shift(); yb = ya + stack.shift();
x = xb + stack.shift(); y = yb + stack.shift();
bezierCurveTo(xa, ya, xb, yb, x, y);
xa = x + stack.shift(); ya = y + stack.shift();
xb = xa + stack.shift(); yb = ya + stack.shift();
x = xb + stack.shift(); y = yb + stack.shift();
bezierCurveTo(xa, ya, xb, yb, x, y);
stack.pop(); // fd
break;
case 36: // hflex1
xa = x + stack.shift(); y1 = y + stack.shift();
xb = xa + stack.shift(); y2 = y1 + stack.shift();
x = xb + stack.shift();
bezierCurveTo(xa, y1, xb, y2, x, y2);
xa = x + stack.shift();
xb = xa + stack.shift(); y3 = y2 + stack.shift();
x = xb + stack.shift();
bezierCurveTo(xa, y2, xb, y3, x, y);
break;
case 37: // flex1
var x0 = x, y0 = y;
xa = x + stack.shift(); ya = y + stack.shift();
xb = xa + stack.shift(); yb = ya + stack.shift();
x = xb + stack.shift(); y = yb + stack.shift();
bezierCurveTo(xa, ya, xb, yb, x, y);
xa = x + stack.shift(); ya = y + stack.shift();
xb = xa + stack.shift(); yb = ya + stack.shift();
x = xb; y = yb;
if (Math.abs(x - x0) > Math.abs(y - y0)) {
x += stack.shift();
} else {
y += stack.shift();
}
bezierCurveTo(xa, ya, xb, yb, x, y);
break;
default:
error('unknown operator: 12 ' + v);
}
break;
case 14: // endchar
if (stack.length >= 4) {
var achar = stack.pop();
var bchar = stack.pop();
y = stack.pop();
x = stack.pop();
js.push('c.save();');
js.push('c.translate('+ x + ',' + y + ');');
var gid = lookupCmap(font.cmap, String.fromCharCode(
font.glyphNameMap[Encodings.StandardEncoding[achar]]));
compileCharString(font.glyphs[gid], js, font);
js.push('c.restore();');
gid = lookupCmap(font.cmap, String.fromCharCode(
font.glyphNameMap[Encodings.StandardEncoding[bchar]]));
compileCharString(font.glyphs[gid], js, font);
}
return;
case 18: // hstemhm
stems += stack.length >> 1;
stackClean = true;
break;
case 19: // hintmask
stems += stack.length >> 1;
i += (stems + 7) >> 3;
stackClean = true;
break;
case 20: // cntrmask
stems += stack.length >> 1;
i += (stems + 7) >> 3;
stackClean = true;
break;
case 21: // rmoveto
y += stack.pop();
x += stack.pop();
moveTo(x, y);
stackClean = true;
break;
case 22: // hmoveto
x += stack.pop();
moveTo(x, y);
stackClean = true;
break;
case 23: // vstemhm
stems += stack.length >> 1;
stackClean = true;
break;
case 24: // rcurveline
while (stack.length > 2) {
xa = x + stack.shift(); ya = y + stack.shift();
xb = xa + stack.shift(); yb = ya + stack.shift();
x = xb + stack.shift(); y = yb + stack.shift();
bezierCurveTo(xa, ya, xb, yb, x, y);
}
x += stack.shift();
y += stack.shift();
lineTo(x, y);
break;
case 25: // rlinecurve
while (stack.length > 6) {
x += stack.shift();
y += stack.shift();
lineTo(x, y);
}
xa = x + stack.shift(); ya = y + stack.shift();
xb = xa + stack.shift(); yb = ya + stack.shift();
x = xb + stack.shift(); y = yb + stack.shift();
bezierCurveTo(xa, ya, xb, yb, x, y);
break;
case 26: // vvcurveto
if (stack.length % 2) {
x += stack.shift();
}
while (stack.length > 0) {
xa = x; ya = y + stack.shift();
xb = xa + stack.shift(); yb = ya + stack.shift();
x = xb; y = yb + stack.shift();
bezierCurveTo(xa, ya, xb, yb, x, y);
}
break;
case 27: // hhcurveto
if (stack.length % 2) {
y += stack.shift();
}
while (stack.length > 0) {
xa = x + stack.shift(); ya = y;
xb = xa + stack.shift(); yb = ya + stack.shift();
x = xb + stack.shift(); y = yb;
bezierCurveTo(xa, ya, xb, yb, x, y);
}
break;
case 28:
stack.push(((code[i] << 24) | (code[i + 1] << 16)) >> 16);
i += 2;
break;
case 29: // callgsubr
n = stack.pop() + font.gsubrsBias;
subrCode = font.gsubrs[n];
if (subrCode) {
parse(subrCode);
}
break;
case 30: // vhcurveto
while (stack.length > 0) {
xa = x; ya = y + stack.shift();
xb = xa + stack.shift(); yb = ya + stack.shift();
x = xb + stack.shift();
y = yb + (stack.length === 1 ? stack.shift() : 0);
bezierCurveTo(xa, ya, xb, yb, x, y);
if (stack.length === 0) {
break;
}
xa = x + stack.shift(); ya = y;
xb = xa + stack.shift(); yb = ya + stack.shift();
y = yb + stack.shift();
x = xb + (stack.length === 1 ? stack.shift() : 0);
bezierCurveTo(xa, ya, xb, yb, x, y);
}
break;
case 31: // hvcurveto
while (stack.length > 0) {
xa = x + stack.shift(); ya = y;
xb = xa + stack.shift(); yb = ya + stack.shift();
y = yb + stack.shift();
x = xb + (stack.length === 1 ? stack.shift() : 0);
bezierCurveTo(xa, ya, xb, yb, x, y);
if (stack.length === 0) {
break;
}
xa = x; ya = y + stack.shift();
xb = xa + stack.shift(); yb = ya + stack.shift();
x = xb + stack.shift();
y = yb + (stack.length === 1 ? stack.shift() : 0);
bezierCurveTo(xa, ya, xb, yb, x, y);
}
break;
default:
if (v < 32) {
error('unknown operator: ' + v);
}
if (v < 247) {
stack.push(v - 139);
} else if (v < 251) {
stack.push((v - 247) * 256 + code[i++] + 108);
} else if (v < 255) {
stack.push(-(v - 251) * 256 - code[i++] - 108);
} else {
stack.push(((code[i] << 24) | (code[i + 1] << 16) |
(code[i + 2] << 8) | code[i + 3]) / 65536);
i += 4;
}
break;
}
if (stackClean) {
stack.length = 0;
}
}
}
parse(code);
}
var noop = '';
function CompiledFont(fontMatrix) {
this.compiledGlyphs = {};
this.fontMatrix = fontMatrix;
}
CompiledFont.prototype = {
getPathJs: function (unicode) {
var gid = lookupCmap(this.cmap, unicode);
var fn = this.compiledGlyphs[gid];
if (!fn) {
this.compiledGlyphs[gid] = fn = this.compileGlyph(this.glyphs[gid]);
}
return fn;
},
compileGlyph: function (code) {
if (!code || code.length === 0 || code[0] === 14) {
return noop;
}
var js = [];
js.push('c.save();');
js.push('c.transform(' + this.fontMatrix.join(',') + ');');
js.push('c.scale(size, -size);');
this.compileGlyphImpl(code, js);
js.push('c.restore();');
return js.join('\n');
},
compileGlyphImpl: function () {
error('Children classes should implement this.');
},
hasBuiltPath: function (unicode) {
var gid = lookupCmap(this.cmap, unicode);
return gid in this.compiledGlyphs;
}
};
function TrueTypeCompiled(glyphs, cmap, fontMatrix) {
fontMatrix = fontMatrix || [0.000488, 0, 0, 0.000488, 0, 0];
CompiledFont.call(this, fontMatrix);
this.glyphs = glyphs;
this.cmap = cmap;
this.compiledGlyphs = [];
}
Util.inherit(TrueTypeCompiled, CompiledFont, {
compileGlyphImpl: function (code, js) {
compileGlyf(code, js, this);
}
});
function Type2Compiled(cffInfo, cmap, fontMatrix, glyphNameMap) {
fontMatrix = fontMatrix || [0.001, 0, 0, 0.001, 0, 0];
CompiledFont.call(this, fontMatrix);
this.glyphs = cffInfo.glyphs;
this.gsubrs = cffInfo.gsubrs || [];
this.subrs = cffInfo.subrs || [];
this.cmap = cmap;
this.glyphNameMap = glyphNameMap || GlyphsUnicode;
this.compiledGlyphs = [];
this.gsubrsBias = (this.gsubrs.length < 1240 ?
107 : (this.gsubrs.length < 33900 ? 1131 : 32768));
this.subrsBias = (this.subrs.length < 1240 ?
107 : (this.subrs.length < 33900 ? 1131 : 32768));
}
Util.inherit(Type2Compiled, CompiledFont, {
compileGlyphImpl: function (code, js) {
compileCharString(code, js, this);
}
});
return {
create: function FontRendererFactory_create(font) {
var data = new Uint8Array(font.data);
var cmap, glyf, loca, cff, indexToLocFormat, unitsPerEm;
var numTables = getUshort(data, 4);
for (var i = 0, p = 12; i < numTables; i++, p += 16) {
var tag = bytesToString(data.subarray(p, p + 4));
var offset = getLong(data, p + 8);
var length = getLong(data, p + 12);
switch (tag) {
case 'cmap':
cmap = parseCmap(data, offset, offset + length);
break;
case 'glyf':
glyf = data.subarray(offset, offset + length);
break;
case 'loca':
loca = data.subarray(offset, offset + length);
break;
case 'head':
unitsPerEm = getUshort(data, offset + 18);
indexToLocFormat = getUshort(data, offset + 50);
break;
case 'CFF ':
cff = parseCff(data, offset, offset + length);
break;
}
}
if (glyf) {
var fontMatrix = (!unitsPerEm ? font.fontMatrix :
[1 / unitsPerEm, 0, 0, 1 / unitsPerEm, 0, 0]);
return new TrueTypeCompiled(
parseGlyfTable(glyf, loca, indexToLocFormat), cmap, fontMatrix);
} else {
return new Type2Compiled(cff, cmap, font.fontMatrix, font.glyphNameMap);
}
}
};
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,668 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals assert, ColorSpace, DecodeStream, error, info, isArray, ImageKind,
isStream, JpegStream, JpxImage, Name, Promise, Stream, warn */
'use strict';
var PDFImage = (function PDFImageClosure() {
/**
* Decode the image in the main thread if it supported. Resovles the promise
* when the image data is ready.
*/
function handleImageData(handler, xref, res, image) {
if (image instanceof JpegStream && image.isNativelyDecodable(xref, res)) {
// For natively supported jpegs send them to the main thread for decoding.
var dict = image.dict;
var colorSpace = dict.get('ColorSpace', 'CS');
colorSpace = ColorSpace.parse(colorSpace, xref, res);
var numComps = colorSpace.numComps;
var decodePromise = handler.sendWithPromise('JpegDecode',
[image.getIR(), numComps]);
return decodePromise.then(function (message) {
var data = message.data;
return new Stream(data, 0, data.length, image.dict);
});
} else {
return Promise.resolve(image);
}
}
/**
* Decode and clamp a value. The formula is different from the spec because we
* don't decode to float range [0,1], we decode it in the [0,max] range.
*/
function decodeAndClamp(value, addend, coefficient, max) {
value = addend + value * coefficient;
// Clamp the value to the range
return (value < 0 ? 0 : (value > max ? max : value));
}
function PDFImage(xref, res, image, inline, smask, mask, isMask) {
this.image = image;
var dict = image.dict;
if (dict.has('Filter')) {
var filter = dict.get('Filter').name;
if (filter === 'JPXDecode') {
var jpxImage = new JpxImage();
jpxImage.parseImageProperties(image.stream);
image.stream.reset();
image.bitsPerComponent = jpxImage.bitsPerComponent;
image.numComps = jpxImage.componentsCount;
} else if (filter === 'JBIG2Decode') {
image.bitsPerComponent = 1;
image.numComps = 1;
}
}
// TODO cache rendered images?
this.width = dict.get('Width', 'W');
this.height = dict.get('Height', 'H');
if (this.width < 1 || this.height < 1) {
error('Invalid image width: ' + this.width + ' or height: ' +
this.height);
}
this.interpolate = dict.get('Interpolate', 'I') || false;
this.imageMask = dict.get('ImageMask', 'IM') || false;
this.matte = dict.get('Matte') || false;
var bitsPerComponent = image.bitsPerComponent;
if (!bitsPerComponent) {
bitsPerComponent = dict.get('BitsPerComponent', 'BPC');
if (!bitsPerComponent) {
if (this.imageMask) {
bitsPerComponent = 1;
} else {
error('Bits per component missing in image: ' + this.imageMask);
}
}
}
this.bpc = bitsPerComponent;
if (!this.imageMask) {
var colorSpace = dict.get('ColorSpace', 'CS');
if (!colorSpace) {
info('JPX images (which do not require color spaces)');
switch (image.numComps) {
case 1:
colorSpace = Name.get('DeviceGray');
break;
case 3:
colorSpace = Name.get('DeviceRGB');
break;
case 4:
colorSpace = Name.get('DeviceCMYK');
break;
default:
error('JPX images with ' + this.numComps +
' color components not supported.');
}
}
this.colorSpace = ColorSpace.parse(colorSpace, xref, res);
this.numComps = this.colorSpace.numComps;
}
this.decode = dict.get('Decode', 'D');
this.needsDecode = false;
if (this.decode &&
((this.colorSpace && !this.colorSpace.isDefaultDecode(this.decode)) ||
(isMask && !ColorSpace.isDefaultDecode(this.decode, 1)))) {
this.needsDecode = true;
// Do some preprocessing to avoid more math.
var max = (1 << bitsPerComponent) - 1;
this.decodeCoefficients = [];
this.decodeAddends = [];
for (var i = 0, j = 0; i < this.decode.length; i += 2, ++j) {
var dmin = this.decode[i];
var dmax = this.decode[i + 1];
this.decodeCoefficients[j] = dmax - dmin;
this.decodeAddends[j] = max * dmin;
}
}
if (smask) {
this.smask = new PDFImage(xref, res, smask, false);
} else if (mask) {
if (isStream(mask)) {
this.mask = new PDFImage(xref, res, mask, false, null, null, true);
} else {
// Color key mask (just an array).
this.mask = mask;
}
}
}
/**
* Handles processing of image data and returns the Promise that is resolved
* with a PDFImage when the image is ready to be used.
*/
PDFImage.buildImage = function PDFImage_buildImage(handler, xref,
res, image, inline) {
var imagePromise = handleImageData(handler, xref, res, image);
var smaskPromise;
var maskPromise;
var smask = image.dict.get('SMask');
var mask = image.dict.get('Mask');
if (smask) {
smaskPromise = handleImageData(handler, xref, res, smask);
maskPromise = Promise.resolve(null);
} else {
smaskPromise = Promise.resolve(null);
if (mask) {
if (isStream(mask)) {
maskPromise = handleImageData(handler, xref, res, mask);
} else if (isArray(mask)) {
maskPromise = Promise.resolve(mask);
} else {
warn('Unsupported mask format.');
maskPromise = Promise.resolve(null);
}
} else {
maskPromise = Promise.resolve(null);
}
}
return Promise.all([imagePromise, smaskPromise, maskPromise]).then(
function(results) {
var imageData = results[0];
var smaskData = results[1];
var maskData = results[2];
return new PDFImage(xref, res, imageData, inline, smaskData, maskData);
});
};
/**
* Resize an image using the nearest neighbor algorithm. Currently only
* supports one and three component images.
* @param {TypedArray} pixels The original image with one component.
* @param {Number} bpc Number of bits per component.
* @param {Number} components Number of color components, 1 or 3 is supported.
* @param {Number} w1 Original width.
* @param {Number} h1 Original height.
* @param {Number} w2 New width.
* @param {Number} h2 New height.
* @param {TypedArray} dest (Optional) The destination buffer.
* @param {Number} alpha01 (Optional) Size reserved for the alpha channel.
* @return {TypedArray} Resized image data.
*/
PDFImage.resize = function PDFImage_resize(pixels, bpc, components,
w1, h1, w2, h2, dest, alpha01) {
if (components !== 1 && components !== 3) {
error('Unsupported component count for resizing.');
}
var length = w2 * h2 * components;
var temp = dest ? dest : (bpc <= 8 ? new Uint8Array(length) :
(bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length)));
var xRatio = w1 / w2;
var yRatio = h1 / h2;
var i, j, py, newIndex = 0, oldIndex;
var xScaled = new Uint16Array(w2);
var w1Scanline = w1 * components;
if (alpha01 !== 1) {
alpha01 = 0;
}
for (j = 0; j < w2; j++) {
xScaled[j] = Math.floor(j * xRatio) * components;
}
if (components === 1) {
for (i = 0; i < h2; i++) {
py = Math.floor(i * yRatio) * w1Scanline;
for (j = 0; j < w2; j++) {
oldIndex = py + xScaled[j];
temp[newIndex++] = pixels[oldIndex];
}
}
} else if (components === 3) {
for (i = 0; i < h2; i++) {
py = Math.floor(i * yRatio) * w1Scanline;
for (j = 0; j < w2; j++) {
oldIndex = py + xScaled[j];
temp[newIndex++] = pixels[oldIndex++];
temp[newIndex++] = pixels[oldIndex++];
temp[newIndex++] = pixels[oldIndex++];
newIndex += alpha01;
}
}
}
return temp;
};
PDFImage.createMask =
function PDFImage_createMask(imgArray, width, height,
imageIsFromDecodeStream, inverseDecode) {
// |imgArray| might not contain full data for every pixel of the mask, so
// we need to distinguish between |computedLength| and |actualLength|.
// In particular, if inverseDecode is true, then the array we return must
// have a length of |computedLength|.
var computedLength = ((width + 7) >> 3) * height;
var actualLength = imgArray.byteLength;
var haveFullData = computedLength === actualLength;
var data, i;
if (imageIsFromDecodeStream && (!inverseDecode || haveFullData)) {
// imgArray came from a DecodeStream and its data is in an appropriate
// form, so we can just transfer it.
data = imgArray;
} else if (!inverseDecode) {
data = new Uint8Array(actualLength);
data.set(imgArray);
} else {
data = new Uint8Array(computedLength);
data.set(imgArray);
for (i = actualLength; i < computedLength; i++) {
data[i] = 0xff;
}
}
// If necessary, invert the original mask data (but not any extra we might
// have added above). It's safe to modify the array -- whether it's the
// original or a copy, we're about to transfer it anyway, so nothing else
// in this thread can be relying on its contents.
if (inverseDecode) {
for (i = 0; i < actualLength; i++) {
data[i] = ~data[i];
}
}
return {data: data, width: width, height: height};
};
PDFImage.prototype = {
get drawWidth() {
return Math.max(this.width,
this.smask && this.smask.width || 0,
this.mask && this.mask.width || 0);
},
get drawHeight() {
return Math.max(this.height,
this.smask && this.smask.height || 0,
this.mask && this.mask.height || 0);
},
decodeBuffer: function PDFImage_decodeBuffer(buffer) {
var bpc = this.bpc;
var numComps = this.numComps;
var decodeAddends = this.decodeAddends;
var decodeCoefficients = this.decodeCoefficients;
var max = (1 << bpc) - 1;
var i, ii;
if (bpc === 1) {
// If the buffer needed decode that means it just needs to be inverted.
for (i = 0, ii = buffer.length; i < ii; i++) {
buffer[i] = +!(buffer[i]);
}
return;
}
var index = 0;
for (i = 0, ii = this.width * this.height; i < ii; i++) {
for (var j = 0; j < numComps; j++) {
buffer[index] = decodeAndClamp(buffer[index], decodeAddends[j],
decodeCoefficients[j], max);
index++;
}
}
},
getComponents: function PDFImage_getComponents(buffer) {
var bpc = this.bpc;
// This image doesn't require any extra work.
if (bpc === 8) {
return buffer;
}
var width = this.width;
var height = this.height;
var numComps = this.numComps;
var length = width * height * numComps;
var bufferPos = 0;
var output = (bpc <= 8 ? new Uint8Array(length) :
(bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length)));
var rowComps = width * numComps;
var max = (1 << bpc) - 1;
var i = 0, ii, buf;
if (bpc === 1) {
// Optimization for reading 1 bpc images.
var mask, loop1End, loop2End;
for (var j = 0; j < height; j++) {
loop1End = i + (rowComps & ~7);
loop2End = i + rowComps;
// unroll loop for all full bytes
while (i < loop1End) {
buf = buffer[bufferPos++];
output[i] = (buf >> 7) & 1;
output[i + 1] = (buf >> 6) & 1;
output[i + 2] = (buf >> 5) & 1;
output[i + 3] = (buf >> 4) & 1;
output[i + 4] = (buf >> 3) & 1;
output[i + 5] = (buf >> 2) & 1;
output[i + 6] = (buf >> 1) & 1;
output[i + 7] = buf & 1;
i += 8;
}
// handle remaing bits
if (i < loop2End) {
buf = buffer[bufferPos++];
mask = 128;
while (i < loop2End) {
output[i++] = +!!(buf & mask);
mask >>= 1;
}
}
}
} else {
// The general case that handles all other bpc values.
var bits = 0;
buf = 0;
for (i = 0, ii = length; i < ii; ++i) {
if (i % rowComps === 0) {
buf = 0;
bits = 0;
}
while (bits < bpc) {
buf = (buf << 8) | buffer[bufferPos++];
bits += 8;
}
var remainingBits = bits - bpc;
var value = buf >> remainingBits;
output[i] = (value < 0 ? 0 : (value > max ? max : value));
buf = buf & ((1 << remainingBits) - 1);
bits = remainingBits;
}
}
return output;
},
fillOpacity: function PDFImage_fillOpacity(rgbaBuf, width, height,
actualHeight, image) {
var smask = this.smask;
var mask = this.mask;
var alphaBuf, sw, sh, i, ii, j;
if (smask) {
sw = smask.width;
sh = smask.height;
alphaBuf = new Uint8Array(sw * sh);
smask.fillGrayBuffer(alphaBuf);
if (sw !== width || sh !== height) {
alphaBuf = PDFImage.resize(alphaBuf, smask.bpc, 1, sw, sh, width,
height);
}
} else if (mask) {
if (mask instanceof PDFImage) {
sw = mask.width;
sh = mask.height;
alphaBuf = new Uint8Array(sw * sh);
mask.numComps = 1;
mask.fillGrayBuffer(alphaBuf);
// Need to invert values in rgbaBuf
for (i = 0, ii = sw * sh; i < ii; ++i) {
alphaBuf[i] = 255 - alphaBuf[i];
}
if (sw !== width || sh !== height) {
alphaBuf = PDFImage.resize(alphaBuf, mask.bpc, 1, sw, sh, width,
height);
}
} else if (isArray(mask)) {
// Color key mask: if any of the compontents are outside the range
// then they should be painted.
alphaBuf = new Uint8Array(width * height);
var numComps = this.numComps;
for (i = 0, ii = width * height; i < ii; ++i) {
var opacity = 0;
var imageOffset = i * numComps;
for (j = 0; j < numComps; ++j) {
var color = image[imageOffset + j];
var maskOffset = j * 2;
if (color < mask[maskOffset] || color > mask[maskOffset + 1]) {
opacity = 255;
break;
}
}
alphaBuf[i] = opacity;
}
} else {
error('Unknown mask format.');
}
}
if (alphaBuf) {
for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
rgbaBuf[j] = alphaBuf[i];
}
} else {
// No mask.
for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
rgbaBuf[j] = 255;
}
}
},
undoPreblend: function PDFImage_undoPreblend(buffer, width, height) {
var matte = this.smask && this.smask.matte;
if (!matte) {
return;
}
var matteRgb = this.colorSpace.getRgb(matte, 0);
var matteR = matteRgb[0];
var matteG = matteRgb[1];
var matteB = matteRgb[2];
var length = width * height * 4;
var r, g, b;
for (var i = 0; i < length; i += 4) {
var alpha = buffer[i + 3];
if (alpha === 0) {
// according formula we have to get Infinity in all components
// making it white (typical paper color) should be okay
buffer[i] = 255;
buffer[i + 1] = 255;
buffer[i + 2] = 255;
continue;
}
var k = 255 / alpha;
r = (buffer[i] - matteR) * k + matteR;
g = (buffer[i + 1] - matteG) * k + matteG;
b = (buffer[i + 2] - matteB) * k + matteB;
buffer[i] = r <= 0 ? 0 : r >= 255 ? 255 : r | 0;
buffer[i + 1] = g <= 0 ? 0 : g >= 255 ? 255 : g | 0;
buffer[i + 2] = b <= 0 ? 0 : b >= 255 ? 255 : b | 0;
}
},
createImageData: function PDFImage_createImageData(forceRGBA) {
var drawWidth = this.drawWidth;
var drawHeight = this.drawHeight;
var imgData = { // other fields are filled in below
width: drawWidth,
height: drawHeight
};
var numComps = this.numComps;
var originalWidth = this.width;
var originalHeight = this.height;
var bpc = this.bpc;
// Rows start at byte boundary.
var rowBytes = (originalWidth * numComps * bpc + 7) >> 3;
var imgArray;
if (!forceRGBA) {
// If it is a 1-bit-per-pixel grayscale (i.e. black-and-white) image
// without any complications, we pass a same-sized copy to the main
// thread rather than expanding by 32x to RGBA form. This saves *lots*
// of memory for many scanned documents. It's also much faster.
//
// Similarly, if it is a 24-bit-per pixel RGB image without any
// complications, we avoid expanding by 1.333x to RGBA form.
var kind;
if (this.colorSpace.name === 'DeviceGray' && bpc === 1) {
kind = ImageKind.GRAYSCALE_1BPP;
} else if (this.colorSpace.name === 'DeviceRGB' && bpc === 8 &&
!this.needsDecode) {
kind = ImageKind.RGB_24BPP;
}
if (kind && !this.smask && !this.mask &&
drawWidth === originalWidth && drawHeight === originalHeight) {
imgData.kind = kind;
imgArray = this.getImageBytes(originalHeight * rowBytes);
// If imgArray came from a DecodeStream, we're safe to transfer it
// (and thus neuter it) because it will constitute the entire
// DecodeStream's data. But if it came from a Stream, we need to
// copy it because it'll only be a portion of the Stream's data, and
// the rest will be read later on.
if (this.image instanceof DecodeStream) {
imgData.data = imgArray;
} else {
var newArray = new Uint8Array(imgArray.length);
newArray.set(imgArray);
imgData.data = newArray;
}
if (this.needsDecode) {
// Invert the buffer (which must be grayscale if we reached here).
assert(kind === ImageKind.GRAYSCALE_1BPP);
var buffer = imgData.data;
for (var i = 0, ii = buffer.length; i < ii; i++) {
buffer[i] ^= 0xff;
}
}
return imgData;
}
if (this.image instanceof JpegStream && !this.smask && !this.mask) {
imgData.kind = ImageKind.RGB_24BPP;
imgData.data = this.getImageBytes(originalHeight * rowBytes,
drawWidth, drawHeight, true);
return imgData;
}
}
imgArray = this.getImageBytes(originalHeight * rowBytes);
// imgArray can be incomplete (e.g. after CCITT fax encoding).
var actualHeight = 0 | (imgArray.length / rowBytes *
drawHeight / originalHeight);
var comps = this.getComponents(imgArray);
// If opacity data is present, use RGBA_32BPP form. Otherwise, use the
// more compact RGB_24BPP form if allowable.
var alpha01, maybeUndoPreblend;
if (!forceRGBA && !this.smask && !this.mask) {
imgData.kind = ImageKind.RGB_24BPP;
imgData.data = new Uint8Array(drawWidth * drawHeight * 3);
alpha01 = 0;
maybeUndoPreblend = false;
} else {
imgData.kind = ImageKind.RGBA_32BPP;
imgData.data = new Uint8Array(drawWidth * drawHeight * 4);
alpha01 = 1;
maybeUndoPreblend = true;
// Color key masking (opacity) must be performed before decoding.
this.fillOpacity(imgData.data, drawWidth, drawHeight, actualHeight,
comps);
}
if (this.needsDecode) {
this.decodeBuffer(comps);
}
this.colorSpace.fillRgb(imgData.data, originalWidth, originalHeight,
drawWidth, drawHeight, actualHeight, bpc, comps,
alpha01);
if (maybeUndoPreblend) {
this.undoPreblend(imgData.data, drawWidth, actualHeight);
}
return imgData;
},
fillGrayBuffer: function PDFImage_fillGrayBuffer(buffer) {
var numComps = this.numComps;
if (numComps !== 1) {
error('Reading gray scale from a color image: ' + numComps);
}
var width = this.width;
var height = this.height;
var bpc = this.bpc;
// rows start at byte boundary
var rowBytes = (width * numComps * bpc + 7) >> 3;
var imgArray = this.getImageBytes(height * rowBytes);
var comps = this.getComponents(imgArray);
var i, length;
if (bpc === 1) {
// inline decoding (= inversion) for 1 bpc images
length = width * height;
if (this.needsDecode) {
// invert and scale to {0, 255}
for (i = 0; i < length; ++i) {
buffer[i] = (comps[i] - 1) & 255;
}
} else {
// scale to {0, 255}
for (i = 0; i < length; ++i) {
buffer[i] = (-comps[i]) & 255;
}
}
return;
}
if (this.needsDecode) {
this.decodeBuffer(comps);
}
length = width * height;
// we aren't using a colorspace so we need to scale the value
var scale = 255 / ((1 << bpc) - 1);
for (i = 0; i < length; ++i) {
buffer[i] = (scale * comps[i]) | 0;
}
},
getImageBytes: function PDFImage_getImageBytes(length,
drawWidth, drawHeight,
forceRGB) {
this.image.reset();
this.image.drawWidth = drawWidth || this.width;
this.image.drawHeight = drawHeight || this.height;
this.image.forceRGB = !!forceRGB;
return this.image.getBytes(length);
}
};
return PDFImage;
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,166 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2014 Opera Software ASA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*
* Based on https://code.google.com/p/smhasher/wiki/MurmurHash3.
* Hashes roughly 100 KB per millisecond on i7 3.4 GHz.
*/
/* globals Uint32ArrayView */
'use strict';
var MurmurHash3_64 = (function MurmurHash3_64Closure (seed) {
// Workaround for missing math precison in JS.
var MASK_HIGH = 0xffff0000;
var MASK_LOW = 0xffff;
function MurmurHash3_64 (seed) {
var SEED = 0xc3d2e1f0;
this.h1 = seed ? seed & 0xffffffff : SEED;
this.h2 = seed ? seed & 0xffffffff : SEED;
}
var alwaysUseUint32ArrayView = false;
//#if !(FIREFOX || MOZCENTRAL || B2G || CHROME)
// old webkits have issues with non-aligned arrays
try {
new Uint32Array(new Uint8Array(5).buffer, 0, 1);
} catch (e) {
alwaysUseUint32ArrayView = true;
}
//#endif
MurmurHash3_64.prototype = {
update: function MurmurHash3_64_update(input) {
var useUint32ArrayView = alwaysUseUint32ArrayView;
var i;
if (typeof input === 'string') {
var data = new Uint8Array(input.length * 2);
var length = 0;
for (i = 0; i < input.length; i++) {
var code = input.charCodeAt(i);
if (code <= 0xff) {
data[length++] = code;
}
else {
data[length++] = code >>> 8;
data[length++] = code & 0xff;
}
}
} else if (input instanceof Uint8Array) {
data = input;
length = data.length;
} else if (typeof input === 'object' && ('length' in input)) {
// processing regular arrays as well, e.g. for IE9
data = input;
length = data.length;
useUint32ArrayView = true;
} else {
throw new Error('Wrong data format in MurmurHash3_64_update. ' +
'Input must be a string or array.');
}
var blockCounts = length >> 2;
var tailLength = length - blockCounts * 4;
// we don't care about endianness here
var dataUint32 = useUint32ArrayView ?
new Uint32ArrayView(data, blockCounts) :
new Uint32Array(data.buffer, 0, blockCounts);
var k1 = 0;
var k2 = 0;
var h1 = this.h1;
var h2 = this.h2;
var C1 = 0xcc9e2d51;
var C2 = 0x1b873593;
var C1_LOW = C1 & MASK_LOW;
var C2_LOW = C2 & MASK_LOW;
for (i = 0; i < blockCounts; i++) {
if (i & 1) {
k1 = dataUint32[i];
k1 = (k1 * C1 & MASK_HIGH) | (k1 * C1_LOW & MASK_LOW);
k1 = k1 << 15 | k1 >>> 17;
k1 = (k1 * C2 & MASK_HIGH) | (k1 * C2_LOW & MASK_LOW);
h1 ^= k1;
h1 = h1 << 13 | h1 >>> 19;
h1 = h1 * 5 + 0xe6546b64;
} else {
k2 = dataUint32[i];
k2 = (k2 * C1 & MASK_HIGH) | (k2 * C1_LOW & MASK_LOW);
k2 = k2 << 15 | k2 >>> 17;
k2 = (k2 * C2 & MASK_HIGH) | (k2 * C2_LOW & MASK_LOW);
h2 ^= k2;
h2 = h2 << 13 | h2 >>> 19;
h2 = h2 * 5 + 0xe6546b64;
}
}
k1 = 0;
switch (tailLength) {
case 3:
k1 ^= data[blockCounts * 4 + 2] << 16;
/* falls through */
case 2:
k1 ^= data[blockCounts * 4 + 1] << 8;
/* falls through */
case 1:
k1 ^= data[blockCounts * 4];
/* falls through */
k1 = (k1 * C1 & MASK_HIGH) | (k1 * C1_LOW & MASK_LOW);
k1 = k1 << 15 | k1 >>> 17;
k1 = (k1 * C2 & MASK_HIGH) | (k1 * C2_LOW & MASK_LOW);
if (blockCounts & 1) {
h1 ^= k1;
} else {
h2 ^= k1;
}
}
this.h1 = h1;
this.h2 = h2;
return this;
},
hexdigest: function MurmurHash3_64_hexdigest () {
var h1 = this.h1;
var h2 = this.h2;
h1 ^= h2 >>> 1;
h1 = (h1 * 0xed558ccd & MASK_HIGH) | (h1 * 0x8ccd & MASK_LOW);
h2 = (h2 * 0xff51afd7 & MASK_HIGH) |
(((h2 << 16 | h1 >>> 16) * 0xafd7ed55 & MASK_HIGH) >>> 16);
h1 ^= h2 >>> 1;
h1 = (h1 * 0x1a85ec53 & MASK_HIGH) | (h1 * 0xec53 & MASK_LOW);
h2 = (h2 * 0xc4ceb9fe & MASK_HIGH) |
(((h2 << 16 | h1 >>> 16) * 0xb9fe1a85 & MASK_HIGH) >>> 16);
h1 ^= h2 >>> 1;
for (var i = 0, arr = [h1, h2], str = ''; i < arr.length; i++) {
var hex = (arr[i] >>> 0).toString(16);
while (hex.length < 8) {
hex = '0' + hex;
}
str += hex;
}
return str;
}
};
return MurmurHash3_64;
})();
@@ -0,0 +1,277 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// NOTE: Be careful what goes in this file, as it is also used from the context
// of the addon. So using warn/error in here will break the addon.
'use strict';
//#if (FIREFOX || MOZCENTRAL)
//
//Components.utils.import('resource://gre/modules/Services.jsm');
//
//var EXPORTED_SYMBOLS = ['NetworkManager'];
//
//var console = {
// log: function console_log(aMsg) {
// var msg = 'network.js: ' + (aMsg.join ? aMsg.join('') : aMsg);
// Services.console.logStringMessage(msg);
// // TODO(mack): dump() doesn't seem to work here...
// dump(msg + '\n');
// }
//}
//#endif
var NetworkManager = (function NetworkManagerClosure() {
var OK_RESPONSE = 200;
var PARTIAL_CONTENT_RESPONSE = 206;
function NetworkManager(url, args) {
this.url = url;
args = args || {};
this.isHttp = /^https?:/i.test(url);
this.httpHeaders = (this.isHttp && args.httpHeaders) || {};
this.withCredentials = args.withCredentials || false;
this.getXhr = args.getXhr ||
function NetworkManager_getXhr() {
//#if B2G
// return new XMLHttpRequest({ mozSystem: true });
//#else
return new XMLHttpRequest();
//#endif
};
this.currXhrId = 0;
this.pendingRequests = {};
this.loadedRequests = {};
}
function getArrayBuffer(xhr) {
var data = xhr.response;
if (typeof data !== 'string') {
return data;
}
var length = data.length;
var array = new Uint8Array(length);
for (var i = 0; i < length; i++) {
array[i] = data.charCodeAt(i) & 0xFF;
}
return array.buffer;
}
NetworkManager.prototype = {
requestRange: function NetworkManager_requestRange(begin, end, listeners) {
var args = {
begin: begin,
end: end
};
for (var prop in listeners) {
args[prop] = listeners[prop];
}
return this.request(args);
},
requestFull: function NetworkManager_requestFull(listeners) {
return this.request(listeners);
},
request: function NetworkManager_request(args) {
var xhr = this.getXhr();
var xhrId = this.currXhrId++;
var pendingRequest = this.pendingRequests[xhrId] = {
xhr: xhr
};
xhr.open('GET', this.url);
xhr.withCredentials = this.withCredentials;
for (var property in this.httpHeaders) {
var value = this.httpHeaders[property];
if (typeof value === 'undefined') {
continue;
}
xhr.setRequestHeader(property, value);
}
if (this.isHttp && 'begin' in args && 'end' in args) {
var rangeStr = args.begin + '-' + (args.end - 1);
xhr.setRequestHeader('Range', 'bytes=' + rangeStr);
pendingRequest.expectedStatus = 206;
} else {
pendingRequest.expectedStatus = 200;
}
if (args.onProgressiveData) {
// Some legacy browsers might throw an exception.
try {
xhr.responseType = 'moz-chunked-arraybuffer';
} catch(e) {}
if (xhr.responseType === 'moz-chunked-arraybuffer') {
pendingRequest.onProgressiveData = args.onProgressiveData;
pendingRequest.mozChunked = true;
} else {
xhr.responseType = 'arraybuffer';
}
} else {
xhr.responseType = 'arraybuffer';
}
if (args.onError) {
xhr.onerror = function(evt) {
args.onError(xhr.status);
};
}
xhr.onreadystatechange = this.onStateChange.bind(this, xhrId);
xhr.onprogress = this.onProgress.bind(this, xhrId);
pendingRequest.onHeadersReceived = args.onHeadersReceived;
pendingRequest.onDone = args.onDone;
pendingRequest.onError = args.onError;
pendingRequest.onProgress = args.onProgress;
xhr.send(null);
return xhrId;
},
onProgress: function NetworkManager_onProgress(xhrId, evt) {
var pendingRequest = this.pendingRequests[xhrId];
if (!pendingRequest) {
// Maybe abortRequest was called...
return;
}
if (pendingRequest.mozChunked) {
var chunk = getArrayBuffer(pendingRequest.xhr);
pendingRequest.onProgressiveData(chunk);
}
var onProgress = pendingRequest.onProgress;
if (onProgress) {
onProgress(evt);
}
},
onStateChange: function NetworkManager_onStateChange(xhrId, evt) {
var pendingRequest = this.pendingRequests[xhrId];
if (!pendingRequest) {
// Maybe abortRequest was called...
return;
}
var xhr = pendingRequest.xhr;
if (xhr.readyState >= 2 && pendingRequest.onHeadersReceived) {
pendingRequest.onHeadersReceived();
delete pendingRequest.onHeadersReceived;
}
if (xhr.readyState !== 4) {
return;
}
if (!(xhrId in this.pendingRequests)) {
// The XHR request might have been aborted in onHeadersReceived()
// callback, in which case we should abort request
return;
}
delete this.pendingRequests[xhrId];
// success status == 0 can be on ftp, file and other protocols
if (xhr.status === 0 && this.isHttp) {
if (pendingRequest.onError) {
pendingRequest.onError(xhr.status);
}
return;
}
var xhrStatus = xhr.status || OK_RESPONSE;
// From http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.2:
// "A server MAY ignore the Range header". This means it's possible to
// get a 200 rather than a 206 response from a range request.
var ok_response_on_range_request =
xhrStatus === OK_RESPONSE &&
pendingRequest.expectedStatus === PARTIAL_CONTENT_RESPONSE;
if (!ok_response_on_range_request &&
xhrStatus !== pendingRequest.expectedStatus) {
if (pendingRequest.onError) {
pendingRequest.onError(xhr.status);
}
return;
}
this.loadedRequests[xhrId] = true;
var chunk = getArrayBuffer(xhr);
if (xhrStatus === PARTIAL_CONTENT_RESPONSE) {
var rangeHeader = xhr.getResponseHeader('Content-Range');
var matches = /bytes (\d+)-(\d+)\/(\d+)/.exec(rangeHeader);
var begin = parseInt(matches[1], 10);
pendingRequest.onDone({
begin: begin,
chunk: chunk
});
} else if (pendingRequest.onProgressiveData) {
pendingRequest.onDone(null);
} else {
pendingRequest.onDone({
begin: 0,
chunk: chunk
});
}
},
hasPendingRequests: function NetworkManager_hasPendingRequests() {
for (var xhrId in this.pendingRequests) {
return true;
}
return false;
},
getRequestXhr: function NetworkManager_getXhr(xhrId) {
return this.pendingRequests[xhrId].xhr;
},
isStreamingRequest: function NetworkManager_isStreamingRequest(xhrId) {
return !!(this.pendingRequests[xhrId].onProgressiveData);
},
isPendingRequest: function NetworkManager_isPendingRequest(xhrId) {
return xhrId in this.pendingRequests;
},
isLoadedRequest: function NetworkManager_isLoadedRequest(xhrId) {
return xhrId in this.loadedRequests;
},
abortAllRequests: function NetworkManager_abortAllRequests() {
for (var xhrId in this.pendingRequests) {
this.abortRequest(xhrId | 0);
}
},
abortRequest: function NetworkManager_abortRequest(xhrId) {
var xhr = this.pendingRequests[xhrId].xhr;
delete this.pendingRequests[xhrId];
xhr.abort();
}
};
return NetworkManager;
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,804 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals ColorSpace, PDFFunction, Util, error, warn, info, isArray, isStream,
assert, isPDFFunction, UnsupportedManager, UNSUPPORTED_FEATURES,
MissingDataException */
'use strict';
var PatternType = {
FUNCTION_BASED: 1,
AXIAL: 2,
RADIAL: 3,
FREE_FORM_MESH: 4,
LATTICE_FORM_MESH: 5,
COONS_PATCH_MESH: 6,
TENSOR_PATCH_MESH: 7
};
var Pattern = (function PatternClosure() {
// Constructor should define this.getPattern
function Pattern() {
error('should not call Pattern constructor');
}
Pattern.prototype = {
// Input: current Canvas context
// Output: the appropriate fillStyle or strokeStyle
getPattern: function Pattern_getPattern(ctx) {
error('Should not call Pattern.getStyle: ' + ctx);
}
};
Pattern.parseShading = function Pattern_parseShading(shading, matrix, xref,
res) {
var dict = isStream(shading) ? shading.dict : shading;
var type = dict.get('ShadingType');
try {
switch (type) {
case PatternType.AXIAL:
case PatternType.RADIAL:
// Both radial and axial shadings are handled by RadialAxial shading.
return new Shadings.RadialAxial(dict, matrix, xref, res);
case PatternType.FREE_FORM_MESH:
case PatternType.LATTICE_FORM_MESH:
case PatternType.COONS_PATCH_MESH:
case PatternType.TENSOR_PATCH_MESH:
return new Shadings.Mesh(shading, matrix, xref, res);
default:
throw new Error('Unknown PatternType: ' + type);
}
} catch (ex) {
if (ex instanceof MissingDataException) {
throw ex;
}
UnsupportedManager.notify(UNSUPPORTED_FEATURES.shadingPattern);
warn(ex);
return new Shadings.Dummy();
}
};
return Pattern;
})();
var Shadings = {};
// A small number to offset the first/last color stops so we can insert ones to
// support extend. Number.MIN_VALUE appears to be too small and breaks the
// extend. 1e-7 works in FF but chrome seems to use an even smaller sized number
// internally so we have to go bigger.
Shadings.SMALL_NUMBER = 1e-2;
// Radial and axial shading have very similar implementations
// If needed, the implementations can be broken into two classes
Shadings.RadialAxial = (function RadialAxialClosure() {
function RadialAxial(dict, matrix, xref, res) {
this.matrix = matrix;
this.coordsArr = dict.get('Coords');
this.shadingType = dict.get('ShadingType');
this.type = 'Pattern';
var cs = dict.get('ColorSpace', 'CS');
cs = ColorSpace.parse(cs, xref, res);
this.cs = cs;
var t0 = 0.0, t1 = 1.0;
if (dict.has('Domain')) {
var domainArr = dict.get('Domain');
t0 = domainArr[0];
t1 = domainArr[1];
}
var extendStart = false, extendEnd = false;
if (dict.has('Extend')) {
var extendArr = dict.get('Extend');
extendStart = extendArr[0];
extendEnd = extendArr[1];
}
if (this.shadingType === PatternType.RADIAL &&
(!extendStart || !extendEnd)) {
// Radial gradient only currently works if either circle is fully within
// the other circle.
var x1 = this.coordsArr[0];
var y1 = this.coordsArr[1];
var r1 = this.coordsArr[2];
var x2 = this.coordsArr[3];
var y2 = this.coordsArr[4];
var r2 = this.coordsArr[5];
var distance = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
if (r1 <= r2 + distance &&
r2 <= r1 + distance) {
warn('Unsupported radial gradient.');
}
}
this.extendStart = extendStart;
this.extendEnd = extendEnd;
var fnObj = dict.get('Function');
var fn = PDFFunction.parseArray(xref, fnObj);
// 10 samples seems good enough for now, but probably won't work
// if there are sharp color changes. Ideally, we would implement
// the spec faithfully and add lossless optimizations.
var diff = t1 - t0;
var step = diff / 10;
var colorStops = this.colorStops = [];
// Protect against bad domains so we don't end up in an infinte loop below.
if (t0 >= t1 || step <= 0) {
// Acrobat doesn't seem to handle these cases so we'll ignore for
// now.
info('Bad shading domain.');
return;
}
var color = new Float32Array(cs.numComps), ratio = new Float32Array(1);
var rgbColor;
for (var i = t0; i <= t1; i += step) {
ratio[0] = i;
fn(ratio, 0, color, 0);
rgbColor = cs.getRgb(color, 0);
var cssColor = Util.makeCssRgb(rgbColor[0], rgbColor[1], rgbColor[2]);
colorStops.push([(i - t0) / diff, cssColor]);
}
var background = 'transparent';
if (dict.has('Background')) {
rgbColor = cs.getRgb(dict.get('Background'), 0);
background = Util.makeCssRgb(rgbColor[0], rgbColor[1], rgbColor[2]);
}
if (!extendStart) {
// Insert a color stop at the front and offset the first real color stop
// so it doesn't conflict with the one we insert.
colorStops.unshift([0, background]);
colorStops[1][0] += Shadings.SMALL_NUMBER;
}
if (!extendEnd) {
// Same idea as above in extendStart but for the end.
colorStops[colorStops.length - 1][0] -= Shadings.SMALL_NUMBER;
colorStops.push([1, background]);
}
this.colorStops = colorStops;
}
RadialAxial.prototype = {
getIR: function RadialAxial_getIR() {
var coordsArr = this.coordsArr;
var shadingType = this.shadingType;
var type, p0, p1, r0, r1;
if (shadingType === PatternType.AXIAL) {
p0 = [coordsArr[0], coordsArr[1]];
p1 = [coordsArr[2], coordsArr[3]];
r0 = null;
r1 = null;
type = 'axial';
} else if (shadingType === PatternType.RADIAL) {
p0 = [coordsArr[0], coordsArr[1]];
p1 = [coordsArr[3], coordsArr[4]];
r0 = coordsArr[2];
r1 = coordsArr[5];
type = 'radial';
} else {
error('getPattern type unknown: ' + shadingType);
}
var matrix = this.matrix;
if (matrix) {
p0 = Util.applyTransform(p0, matrix);
p1 = Util.applyTransform(p1, matrix);
}
return ['RadialAxial', type, this.colorStops, p0, p1, r0, r1];
}
};
return RadialAxial;
})();
// All mesh shading. For now, they will be presented as set of the triangles
// to be drawn on the canvas and rgb color for each vertex.
Shadings.Mesh = (function MeshClosure() {
function MeshStreamReader(stream, context) {
this.stream = stream;
this.context = context;
this.buffer = 0;
this.bufferLength = 0;
var numComps = context.numComps;
this.tmpCompsBuf = new Float32Array(numComps);
var csNumComps = context.colorSpace;
this.tmpCsCompsBuf = context.colorFn ? new Float32Array(csNumComps) :
this.tmpCompsBuf;
}
MeshStreamReader.prototype = {
get hasData() {
if (this.stream.end) {
return this.stream.pos < this.stream.end;
}
if (this.bufferLength > 0) {
return true;
}
var nextByte = this.stream.getByte();
if (nextByte < 0) {
return false;
}
this.buffer = nextByte;
this.bufferLength = 8;
return true;
},
readBits: function MeshStreamReader_readBits(n) {
var buffer = this.buffer;
var bufferLength = this.bufferLength;
if (n === 32) {
if (bufferLength === 0) {
return ((this.stream.getByte() << 24) |
(this.stream.getByte() << 16) | (this.stream.getByte() << 8) |
this.stream.getByte()) >>> 0;
}
buffer = (buffer << 24) | (this.stream.getByte() << 16) |
(this.stream.getByte() << 8) | this.stream.getByte();
var nextByte = this.stream.getByte();
this.buffer = nextByte & ((1 << bufferLength) - 1);
return ((buffer << (8 - bufferLength)) |
((nextByte & 0xFF) >> bufferLength)) >>> 0;
}
if (n === 8 && bufferLength === 0) {
return this.stream.getByte();
}
while (bufferLength < n) {
buffer = (buffer << 8) | this.stream.getByte();
bufferLength += 8;
}
bufferLength -= n;
this.bufferLength = bufferLength;
this.buffer = buffer & ((1 << bufferLength) - 1);
return buffer >> bufferLength;
},
align: function MeshStreamReader_align() {
this.buffer = 0;
this.bufferLength = 0;
},
readFlag: function MeshStreamReader_readFlag() {
return this.readBits(this.context.bitsPerFlag);
},
readCoordinate: function MeshStreamReader_readCoordinate() {
var bitsPerCoordinate = this.context.bitsPerCoordinate;
var xi = this.readBits(bitsPerCoordinate);
var yi = this.readBits(bitsPerCoordinate);
var decode = this.context.decode;
var scale = bitsPerCoordinate < 32 ? 1 / ((1 << bitsPerCoordinate) - 1) :
2.3283064365386963e-10; // 2 ^ -32
return [
xi * scale * (decode[1] - decode[0]) + decode[0],
yi * scale * (decode[3] - decode[2]) + decode[2]
];
},
readComponents: function MeshStreamReader_readComponents() {
var numComps = this.context.numComps;
var bitsPerComponent = this.context.bitsPerComponent;
var scale = bitsPerComponent < 32 ? 1 / ((1 << bitsPerComponent) - 1) :
2.3283064365386963e-10; // 2 ^ -32
var decode = this.context.decode;
var components = this.tmpCompsBuf;
for (var i = 0, j = 4; i < numComps; i++, j += 2) {
var ci = this.readBits(bitsPerComponent);
components[i] = ci * scale * (decode[j + 1] - decode[j]) + decode[j];
}
var color = this.tmpCsCompsBuf;
if (this.context.colorFn) {
this.context.colorFn(components, 0, color, 0);
}
return this.context.colorSpace.getRgb(color, 0);
}
};
function decodeType4Shading(mesh, reader) {
var coords = mesh.coords;
var colors = mesh.colors;
var operators = [];
var ps = []; // not maintaining cs since that will match ps
var verticesLeft = 0; // assuming we have all data to start a new triangle
while (reader.hasData) {
var f = reader.readFlag();
var coord = reader.readCoordinate();
var color = reader.readComponents();
if (verticesLeft === 0) { // ignoring flags if we started a triangle
assert(0 <= f && f <= 2, 'Unknown type4 flag');
switch (f) {
case 0:
verticesLeft = 3;
break;
case 1:
ps.push(ps[ps.length - 2], ps[ps.length - 1]);
verticesLeft = 1;
break;
case 2:
ps.push(ps[ps.length - 3], ps[ps.length - 1]);
verticesLeft = 1;
break;
}
operators.push(f);
}
ps.push(coords.length);
coords.push(coord);
colors.push(color);
verticesLeft--;
reader.align();
}
var psPacked = new Int32Array(ps);
mesh.figures.push({
type: 'triangles',
coords: psPacked,
colors: psPacked
});
}
function decodeType5Shading(mesh, reader, verticesPerRow) {
var coords = mesh.coords;
var colors = mesh.colors;
var ps = []; // not maintaining cs since that will match ps
while (reader.hasData) {
var coord = reader.readCoordinate();
var color = reader.readComponents();
ps.push(coords.length);
coords.push(coord);
colors.push(color);
}
var psPacked = new Int32Array(ps);
mesh.figures.push({
type: 'lattice',
coords: psPacked,
colors: psPacked,
verticesPerRow: verticesPerRow
});
}
var MIN_SPLIT_PATCH_CHUNKS_AMOUNT = 3;
var MAX_SPLIT_PATCH_CHUNKS_AMOUNT = 20;
var TRIANGLE_DENSITY = 20; // count of triangles per entire mesh bounds
var getB = (function getBClosure() {
function buildB(count) {
var lut = [];
for (var i = 0; i <= count; i++) {
var t = i / count, t_ = 1 - t;
lut.push(new Float32Array([t_ * t_ * t_, 3 * t * t_ * t_,
3 * t * t * t_, t * t * t]));
}
return lut;
}
var cache = [];
return function getB(count) {
if (!cache[count]) {
cache[count] = buildB(count);
}
return cache[count];
};
})();
function buildFigureFromPatch(mesh, index) {
var figure = mesh.figures[index];
assert(figure.type === 'patch', 'Unexpected patch mesh figure');
var coords = mesh.coords, colors = mesh.colors;
var pi = figure.coords;
var ci = figure.colors;
var figureMinX = Math.min(coords[pi[0]][0], coords[pi[3]][0],
coords[pi[12]][0], coords[pi[15]][0]);
var figureMinY = Math.min(coords[pi[0]][1], coords[pi[3]][1],
coords[pi[12]][1], coords[pi[15]][1]);
var figureMaxX = Math.max(coords[pi[0]][0], coords[pi[3]][0],
coords[pi[12]][0], coords[pi[15]][0]);
var figureMaxY = Math.max(coords[pi[0]][1], coords[pi[3]][1],
coords[pi[12]][1], coords[pi[15]][1]);
var splitXBy = Math.ceil((figureMaxX - figureMinX) * TRIANGLE_DENSITY /
(mesh.bounds[2] - mesh.bounds[0]));
splitXBy = Math.max(MIN_SPLIT_PATCH_CHUNKS_AMOUNT,
Math.min(MAX_SPLIT_PATCH_CHUNKS_AMOUNT, splitXBy));
var splitYBy = Math.ceil((figureMaxY - figureMinY) * TRIANGLE_DENSITY /
(mesh.bounds[3] - mesh.bounds[1]));
splitYBy = Math.max(MIN_SPLIT_PATCH_CHUNKS_AMOUNT,
Math.min(MAX_SPLIT_PATCH_CHUNKS_AMOUNT, splitYBy));
var verticesPerRow = splitXBy + 1;
var figureCoords = new Int32Array((splitYBy + 1) * verticesPerRow);
var figureColors = new Int32Array((splitYBy + 1) * verticesPerRow);
var k = 0;
var cl = new Uint8Array(3), cr = new Uint8Array(3);
var c0 = colors[ci[0]], c1 = colors[ci[1]],
c2 = colors[ci[2]], c3 = colors[ci[3]];
var bRow = getB(splitYBy), bCol = getB(splitXBy);
for (var row = 0; row <= splitYBy; row++) {
cl[0] = ((c0[0] * (splitYBy - row) + c2[0] * row) / splitYBy) | 0;
cl[1] = ((c0[1] * (splitYBy - row) + c2[1] * row) / splitYBy) | 0;
cl[2] = ((c0[2] * (splitYBy - row) + c2[2] * row) / splitYBy) | 0;
cr[0] = ((c1[0] * (splitYBy - row) + c3[0] * row) / splitYBy) | 0;
cr[1] = ((c1[1] * (splitYBy - row) + c3[1] * row) / splitYBy) | 0;
cr[2] = ((c1[2] * (splitYBy - row) + c3[2] * row) / splitYBy) | 0;
for (var col = 0; col <= splitXBy; col++, k++) {
if ((row === 0 || row === splitYBy) &&
(col === 0 || col === splitXBy)) {
continue;
}
var x = 0, y = 0;
var q = 0;
for (var i = 0; i <= 3; i++) {
for (var j = 0; j <= 3; j++, q++) {
var m = bRow[row][i] * bCol[col][j];
x += coords[pi[q]][0] * m;
y += coords[pi[q]][1] * m;
}
}
figureCoords[k] = coords.length;
coords.push([x, y]);
figureColors[k] = colors.length;
var newColor = new Uint8Array(3);
newColor[0] = ((cl[0] * (splitXBy - col) + cr[0] * col) / splitXBy) | 0;
newColor[1] = ((cl[1] * (splitXBy - col) + cr[1] * col) / splitXBy) | 0;
newColor[2] = ((cl[2] * (splitXBy - col) + cr[2] * col) / splitXBy) | 0;
colors.push(newColor);
}
}
figureCoords[0] = pi[0];
figureColors[0] = ci[0];
figureCoords[splitXBy] = pi[3];
figureColors[splitXBy] = ci[1];
figureCoords[verticesPerRow * splitYBy] = pi[12];
figureColors[verticesPerRow * splitYBy] = ci[2];
figureCoords[verticesPerRow * splitYBy + splitXBy] = pi[15];
figureColors[verticesPerRow * splitYBy + splitXBy] = ci[3];
mesh.figures[index] = {
type: 'lattice',
coords: figureCoords,
colors: figureColors,
verticesPerRow: verticesPerRow
};
}
function decodeType6Shading(mesh, reader) {
// A special case of Type 7. The p11, p12, p21, p22 automatically filled
var coords = mesh.coords;
var colors = mesh.colors;
var ps = new Int32Array(16); // p00, p10, ..., p30, p01, ..., p33
var cs = new Int32Array(4); // c00, c30, c03, c33
while (reader.hasData) {
var f = reader.readFlag();
assert(0 <= f && f <= 3, 'Unknown type6 flag');
var i, ii;
var pi = coords.length;
for (i = 0, ii = (f !== 0 ? 8 : 12); i < ii; i++) {
coords.push(reader.readCoordinate());
}
var ci = colors.length;
for (i = 0, ii = (f !== 0 ? 2 : 4); i < ii; i++) {
colors.push(reader.readComponents());
}
var tmp1, tmp2, tmp3, tmp4;
switch (f) {
case 0:
ps[12] = pi + 3; ps[13] = pi + 4; ps[14] = pi + 5; ps[15] = pi + 6;
ps[ 8] = pi + 2; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 7;
ps[ 4] = pi + 1; /* calculated below */ ps[ 7] = pi + 8;
ps[ 0] = pi; ps[ 1] = pi + 11; ps[ 2] = pi + 10; ps[ 3] = pi + 9;
cs[2] = ci + 1; cs[3] = ci + 2;
cs[0] = ci; cs[1] = ci + 3;
break;
case 1:
tmp1 = ps[12]; tmp2 = ps[13]; tmp3 = ps[14]; tmp4 = ps[15];
ps[12] = pi + 5; ps[13] = pi + 4; ps[14] = pi + 3; ps[15] = pi + 2;
ps[ 8] = pi + 6; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 1;
ps[ 4] = pi + 7; /* calculated below */ ps[ 7] = pi;
ps[ 0] = tmp1; ps[ 1] = tmp2; ps[ 2] = tmp3; ps[ 3] = tmp4;
tmp1 = cs[2]; tmp2 = cs[3];
cs[2] = ci + 1; cs[3] = ci;
cs[0] = tmp1; cs[1] = tmp2;
break;
case 2:
ps[12] = ps[15]; ps[13] = pi + 7; ps[14] = pi + 6; ps[15] = pi + 5;
ps[ 8] = ps[11]; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 4;
ps[ 4] = ps[7]; /* calculated below */ ps[ 7] = pi + 3;
ps[ 0] = ps[3]; ps[ 1] = pi; ps[ 2] = pi + 1; ps[ 3] = pi + 2;
cs[2] = cs[3]; cs[3] = ci + 1;
cs[0] = cs[1]; cs[1] = ci;
break;
case 3:
ps[12] = ps[0]; ps[13] = ps[1]; ps[14] = ps[2]; ps[15] = ps[3];
ps[ 8] = pi; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 7;
ps[ 4] = pi + 1; /* calculated below */ ps[ 7] = pi + 6;
ps[ 0] = pi + 2; ps[ 1] = pi + 3; ps[ 2] = pi + 4; ps[ 3] = pi + 5;
cs[2] = cs[0]; cs[3] = cs[1];
cs[0] = ci; cs[1] = ci + 1;
break;
}
// set p11, p12, p21, p22
ps[5] = coords.length;
coords.push([
(-4 * coords[ps[0]][0] - coords[ps[15]][0] +
6 * (coords[ps[4]][0] + coords[ps[1]][0]) -
2 * (coords[ps[12]][0] + coords[ps[3]][0]) +
3 * (coords[ps[13]][0] + coords[ps[7]][0])) / 9,
(-4 * coords[ps[0]][1] - coords[ps[15]][1] +
6 * (coords[ps[4]][1] + coords[ps[1]][1]) -
2 * (coords[ps[12]][1] + coords[ps[3]][1]) +
3 * (coords[ps[13]][1] + coords[ps[7]][1])) / 9
]);
ps[6] = coords.length;
coords.push([
(-4 * coords[ps[3]][0] - coords[ps[12]][0] +
6 * (coords[ps[2]][0] + coords[ps[7]][0]) -
2 * (coords[ps[0]][0] + coords[ps[15]][0]) +
3 * (coords[ps[4]][0] + coords[ps[14]][0])) / 9,
(-4 * coords[ps[3]][1] - coords[ps[12]][1] +
6 * (coords[ps[2]][1] + coords[ps[7]][1]) -
2 * (coords[ps[0]][1] + coords[ps[15]][1]) +
3 * (coords[ps[4]][1] + coords[ps[14]][1])) / 9
]);
ps[9] = coords.length;
coords.push([
(-4 * coords[ps[12]][0] - coords[ps[3]][0] +
6 * (coords[ps[8]][0] + coords[ps[13]][0]) -
2 * (coords[ps[0]][0] + coords[ps[15]][0]) +
3 * (coords[ps[11]][0] + coords[ps[1]][0])) / 9,
(-4 * coords[ps[12]][1] - coords[ps[3]][1] +
6 * (coords[ps[8]][1] + coords[ps[13]][1]) -
2 * (coords[ps[0]][1] + coords[ps[15]][1]) +
3 * (coords[ps[11]][1] + coords[ps[1]][1])) / 9
]);
ps[10] = coords.length;
coords.push([
(-4 * coords[ps[15]][0] - coords[ps[0]][0] +
6 * (coords[ps[11]][0] + coords[ps[14]][0]) -
2 * (coords[ps[12]][0] + coords[ps[3]][0]) +
3 * (coords[ps[2]][0] + coords[ps[8]][0])) / 9,
(-4 * coords[ps[15]][1] - coords[ps[0]][1] +
6 * (coords[ps[11]][1] + coords[ps[14]][1]) -
2 * (coords[ps[12]][1] + coords[ps[3]][1]) +
3 * (coords[ps[2]][1] + coords[ps[8]][1])) / 9
]);
mesh.figures.push({
type: 'patch',
coords: new Int32Array(ps), // making copies of ps and cs
colors: new Int32Array(cs)
});
}
}
function decodeType7Shading(mesh, reader) {
var coords = mesh.coords;
var colors = mesh.colors;
var ps = new Int32Array(16); // p00, p10, ..., p30, p01, ..., p33
var cs = new Int32Array(4); // c00, c30, c03, c33
while (reader.hasData) {
var f = reader.readFlag();
assert(0 <= f && f <= 3, 'Unknown type7 flag');
var i, ii;
var pi = coords.length;
for (i = 0, ii = (f !== 0 ? 12 : 16); i < ii; i++) {
coords.push(reader.readCoordinate());
}
var ci = colors.length;
for (i = 0, ii = (f !== 0 ? 2 : 4); i < ii; i++) {
colors.push(reader.readComponents());
}
var tmp1, tmp2, tmp3, tmp4;
switch (f) {
case 0:
ps[12] = pi + 3; ps[13] = pi + 4; ps[14] = pi + 5; ps[15] = pi + 6;
ps[ 8] = pi + 2; ps[ 9] = pi + 13; ps[10] = pi + 14; ps[11] = pi + 7;
ps[ 4] = pi + 1; ps[ 5] = pi + 12; ps[ 6] = pi + 15; ps[ 7] = pi + 8;
ps[ 0] = pi; ps[ 1] = pi + 11; ps[ 2] = pi + 10; ps[ 3] = pi + 9;
cs[2] = ci + 1; cs[3] = ci + 2;
cs[0] = ci; cs[1] = ci + 3;
break;
case 1:
tmp1 = ps[12]; tmp2 = ps[13]; tmp3 = ps[14]; tmp4 = ps[15];
ps[12] = pi + 5; ps[13] = pi + 4; ps[14] = pi + 3; ps[15] = pi + 2;
ps[ 8] = pi + 6; ps[ 9] = pi + 11; ps[10] = pi + 10; ps[11] = pi + 1;
ps[ 4] = pi + 7; ps[ 5] = pi + 8; ps[ 6] = pi + 9; ps[ 7] = pi;
ps[ 0] = tmp1; ps[ 1] = tmp2; ps[ 2] = tmp3; ps[ 3] = tmp4;
tmp1 = cs[2]; tmp2 = cs[3];
cs[2] = ci + 1; cs[3] = ci;
cs[0] = tmp1; cs[1] = tmp2;
break;
case 2:
ps[12] = ps[15]; ps[13] = pi + 7; ps[14] = pi + 6; ps[15] = pi + 5;
ps[ 8] = ps[11]; ps[ 9] = pi + 8; ps[10] = pi + 11; ps[11] = pi + 4;
ps[ 4] = ps[7]; ps[ 5] = pi + 9; ps[ 6] = pi + 10; ps[ 7] = pi + 3;
ps[ 0] = ps[3]; ps[ 1] = pi; ps[ 2] = pi + 1; ps[ 3] = pi + 2;
cs[2] = cs[3]; cs[3] = ci + 1;
cs[0] = cs[1]; cs[1] = ci;
break;
case 3:
ps[12] = ps[0]; ps[13] = ps[1]; ps[14] = ps[2]; ps[15] = ps[3];
ps[ 8] = pi; ps[ 9] = pi + 9; ps[10] = pi + 8; ps[11] = pi + 7;
ps[ 4] = pi + 1; ps[ 5] = pi + 10; ps[ 6] = pi + 11; ps[ 7] = pi + 6;
ps[ 0] = pi + 2; ps[ 1] = pi + 3; ps[ 2] = pi + 4; ps[ 3] = pi + 5;
cs[2] = cs[0]; cs[3] = cs[1];
cs[0] = ci; cs[1] = ci + 1;
break;
}
mesh.figures.push({
type: 'patch',
coords: new Int32Array(ps), // making copies of ps and cs
colors: new Int32Array(cs)
});
}
}
function updateBounds(mesh) {
var minX = mesh.coords[0][0], minY = mesh.coords[0][1],
maxX = minX, maxY = minY;
for (var i = 1, ii = mesh.coords.length; i < ii; i++) {
var x = mesh.coords[i][0], y = mesh.coords[i][1];
minX = minX > x ? x : minX;
minY = minY > y ? y : minY;
maxX = maxX < x ? x : maxX;
maxY = maxY < y ? y : maxY;
}
mesh.bounds = [minX, minY, maxX, maxY];
}
function packData(mesh) {
var i, ii, j, jj;
var coords = mesh.coords;
var coordsPacked = new Float32Array(coords.length * 2);
for (i = 0, j = 0, ii = coords.length; i < ii; i++) {
var xy = coords[i];
coordsPacked[j++] = xy[0];
coordsPacked[j++] = xy[1];
}
mesh.coords = coordsPacked;
var colors = mesh.colors;
var colorsPacked = new Uint8Array(colors.length * 3);
for (i = 0, j = 0, ii = colors.length; i < ii; i++) {
var c = colors[i];
colorsPacked[j++] = c[0];
colorsPacked[j++] = c[1];
colorsPacked[j++] = c[2];
}
mesh.colors = colorsPacked;
var figures = mesh.figures;
for (i = 0, ii = figures.length; i < ii; i++) {
var figure = figures[i], ps = figure.coords, cs = figure.colors;
for (j = 0, jj = ps.length; j < jj; j++) {
ps[j] *= 2;
cs[j] *= 3;
}
}
}
function Mesh(stream, matrix, xref, res) {
assert(isStream(stream), 'Mesh data is not a stream');
var dict = stream.dict;
this.matrix = matrix;
this.shadingType = dict.get('ShadingType');
this.type = 'Pattern';
this.bbox = dict.get('BBox');
var cs = dict.get('ColorSpace', 'CS');
cs = ColorSpace.parse(cs, xref, res);
this.cs = cs;
this.background = dict.has('Background') ?
cs.getRgb(dict.get('Background'), 0) : null;
var fnObj = dict.get('Function');
var fn = fnObj ? PDFFunction.parseArray(xref, fnObj) : null;
this.coords = [];
this.colors = [];
this.figures = [];
var decodeContext = {
bitsPerCoordinate: dict.get('BitsPerCoordinate'),
bitsPerComponent: dict.get('BitsPerComponent'),
bitsPerFlag: dict.get('BitsPerFlag'),
decode: dict.get('Decode'),
colorFn: fn,
colorSpace: cs,
numComps: fn ? 1 : cs.numComps
};
var reader = new MeshStreamReader(stream, decodeContext);
var patchMesh = false;
switch (this.shadingType) {
case PatternType.FREE_FORM_MESH:
decodeType4Shading(this, reader);
break;
case PatternType.LATTICE_FORM_MESH:
var verticesPerRow = dict.get('VerticesPerRow') | 0;
assert(verticesPerRow >= 2, 'Invalid VerticesPerRow');
decodeType5Shading(this, reader, verticesPerRow);
break;
case PatternType.COONS_PATCH_MESH:
decodeType6Shading(this, reader);
patchMesh = true;
break;
case PatternType.TENSOR_PATCH_MESH:
decodeType7Shading(this, reader);
patchMesh = true;
break;
default:
error('Unsupported mesh type.');
break;
}
if (patchMesh) {
// dirty bounds calculation for determining, how dense shall be triangles
updateBounds(this);
for (var i = 0, ii = this.figures.length; i < ii; i++) {
buildFigureFromPatch(this, i);
}
}
// calculate bounds
updateBounds(this);
packData(this);
}
Mesh.prototype = {
getIR: function Mesh_getIR() {
return ['Mesh', this.shadingType, this.coords, this.colors, this.figures,
this.bounds, this.matrix, this.bbox, this.background];
}
};
return Mesh;
})();
Shadings.Dummy = (function DummyClosure() {
function Dummy() {
this.type = 'Pattern';
}
Dummy.prototype = {
getIR: function Dummy_getIR() {
return ['Dummy'];
}
};
return Dummy;
})();
function getTilingPatternIR(operatorList, dict, args) {
var matrix = dict.get('Matrix');
var bbox = dict.get('BBox');
var xstep = dict.get('XStep');
var ystep = dict.get('YStep');
var paintType = dict.get('PaintType');
var tilingType = dict.get('TilingType');
return [
'TilingPattern', args, operatorList, matrix, bbox, xstep, ystep,
paintType, tilingType
];
}
@@ -0,0 +1,225 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals NotImplementedException, MissingDataException, Promise, Stream,
PDFDocument, ChunkedStreamManager, createPromiseCapability */
'use strict';
// The maximum number of bytes fetched per range request
var RANGE_CHUNK_SIZE = 65536;
// TODO(mack): Make use of PDFJS.Util.inherit() when it becomes available
var BasePdfManager = (function BasePdfManagerClosure() {
function BasePdfManager() {
throw new Error('Cannot initialize BaseManagerManager');
}
BasePdfManager.prototype = {
onLoadedStream: function BasePdfManager_onLoadedStream() {
throw new NotImplementedException();
},
ensureDoc: function BasePdfManager_ensureDoc(prop, args) {
return this.ensure(this.pdfDocument, prop, args);
},
ensureXRef: function BasePdfManager_ensureXRef(prop, args) {
return this.ensure(this.pdfDocument.xref, prop, args);
},
ensureCatalog: function BasePdfManager_ensureCatalog(prop, args) {
return this.ensure(this.pdfDocument.catalog, prop, args);
},
getPage: function BasePdfManager_pagePage(pageIndex) {
return this.pdfDocument.getPage(pageIndex);
},
cleanup: function BasePdfManager_cleanup() {
return this.pdfDocument.cleanup();
},
ensure: function BasePdfManager_ensure(obj, prop, args) {
return new NotImplementedException();
},
requestRange: function BasePdfManager_ensure(begin, end) {
return new NotImplementedException();
},
requestLoadedStream: function BasePdfManager_requestLoadedStream() {
return new NotImplementedException();
},
sendProgressiveData: function BasePdfManager_sendProgressiveData(chunk) {
return new NotImplementedException();
},
updatePassword: function BasePdfManager_updatePassword(password) {
this.pdfDocument.xref.password = this.password = password;
if (this._passwordChangedCapability) {
this._passwordChangedCapability.resolve();
}
},
passwordChanged: function BasePdfManager_passwordChanged() {
this._passwordChangedCapability = createPromiseCapability();
return this._passwordChangedCapability.promise;
},
terminate: function BasePdfManager_terminate() {
return new NotImplementedException();
}
};
return BasePdfManager;
})();
var LocalPdfManager = (function LocalPdfManagerClosure() {
function LocalPdfManager(data, password) {
var stream = new Stream(data);
this.pdfDocument = new PDFDocument(this, stream, password);
this._loadedStreamCapability = createPromiseCapability();
this._loadedStreamCapability.resolve(stream);
}
LocalPdfManager.prototype = Object.create(BasePdfManager.prototype);
LocalPdfManager.prototype.constructor = LocalPdfManager;
LocalPdfManager.prototype.ensure =
function LocalPdfManager_ensure(obj, prop, args) {
return new Promise(function (resolve, reject) {
try {
var value = obj[prop];
var result;
if (typeof value === 'function') {
result = value.apply(obj, args);
} else {
result = value;
}
resolve(result);
} catch (e) {
reject(e);
}
});
};
LocalPdfManager.prototype.requestRange =
function LocalPdfManager_requestRange(begin, end) {
return Promise.resolve();
};
LocalPdfManager.prototype.requestLoadedStream =
function LocalPdfManager_requestLoadedStream() {
};
LocalPdfManager.prototype.onLoadedStream =
function LocalPdfManager_getLoadedStream() {
return this._loadedStreamCapability.promise;
};
LocalPdfManager.prototype.terminate =
function LocalPdfManager_terminate() {
return;
};
return LocalPdfManager;
})();
var NetworkPdfManager = (function NetworkPdfManagerClosure() {
function NetworkPdfManager(args, msgHandler) {
this.msgHandler = msgHandler;
var params = {
msgHandler: msgHandler,
httpHeaders: args.httpHeaders,
withCredentials: args.withCredentials,
chunkedViewerLoading: args.chunkedViewerLoading,
disableAutoFetch: args.disableAutoFetch,
initialData: args.initialData
};
this.streamManager = new ChunkedStreamManager(args.length, RANGE_CHUNK_SIZE,
args.url, params);
this.pdfDocument = new PDFDocument(this, this.streamManager.getStream(),
args.password);
}
NetworkPdfManager.prototype = Object.create(BasePdfManager.prototype);
NetworkPdfManager.prototype.constructor = NetworkPdfManager;
NetworkPdfManager.prototype.ensure =
function NetworkPdfManager_ensure(obj, prop, args) {
var pdfManager = this;
return new Promise(function (resolve, reject) {
function ensureHelper() {
try {
var result;
var value = obj[prop];
if (typeof value === 'function') {
result = value.apply(obj, args);
} else {
result = value;
}
resolve(result);
} catch(e) {
if (!(e instanceof MissingDataException)) {
reject(e);
return;
}
pdfManager.streamManager.requestRange(e.begin, e.end, ensureHelper);
}
}
ensureHelper();
});
};
NetworkPdfManager.prototype.requestRange =
function NetworkPdfManager_requestRange(begin, end) {
return new Promise(function (resolve) {
this.streamManager.requestRange(begin, end, function() {
resolve();
});
}.bind(this));
};
NetworkPdfManager.prototype.requestLoadedStream =
function NetworkPdfManager_requestLoadedStream() {
this.streamManager.requestAllChunks();
};
NetworkPdfManager.prototype.sendProgressiveData =
function NetworkPdfManager_sendProgressiveData(chunk) {
this.streamManager.onReceiveData({ chunk: chunk });
};
NetworkPdfManager.prototype.onLoadedStream =
function NetworkPdfManager_getLoadedStream() {
return this.streamManager.onLoadedStream();
};
NetworkPdfManager.prototype.terminate =
function NetworkPdfManager_terminate() {
this.streamManager.networkManager.abortAllRequests();
};
return NetworkPdfManager;
})();
@@ -0,0 +1,222 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2014 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals EOF, error, Lexer */
'use strict';
var PostScriptParser = (function PostScriptParserClosure() {
function PostScriptParser(lexer) {
this.lexer = lexer;
this.operators = [];
this.token = null;
this.prev = null;
}
PostScriptParser.prototype = {
nextToken: function PostScriptParser_nextToken() {
this.prev = this.token;
this.token = this.lexer.getToken();
},
accept: function PostScriptParser_accept(type) {
if (this.token.type === type) {
this.nextToken();
return true;
}
return false;
},
expect: function PostScriptParser_expect(type) {
if (this.accept(type)) {
return true;
}
error('Unexpected symbol: found ' + this.token.type + ' expected ' +
type + '.');
},
parse: function PostScriptParser_parse() {
this.nextToken();
this.expect(PostScriptTokenTypes.LBRACE);
this.parseBlock();
this.expect(PostScriptTokenTypes.RBRACE);
return this.operators;
},
parseBlock: function PostScriptParser_parseBlock() {
while (true) {
if (this.accept(PostScriptTokenTypes.NUMBER)) {
this.operators.push(this.prev.value);
} else if (this.accept(PostScriptTokenTypes.OPERATOR)) {
this.operators.push(this.prev.value);
} else if (this.accept(PostScriptTokenTypes.LBRACE)) {
this.parseCondition();
} else {
return;
}
}
},
parseCondition: function PostScriptParser_parseCondition() {
// Add two place holders that will be updated later
var conditionLocation = this.operators.length;
this.operators.push(null, null);
this.parseBlock();
this.expect(PostScriptTokenTypes.RBRACE);
if (this.accept(PostScriptTokenTypes.IF)) {
// The true block is right after the 'if' so it just falls through on
// true else it jumps and skips the true block.
this.operators[conditionLocation] = this.operators.length;
this.operators[conditionLocation + 1] = 'jz';
} else if (this.accept(PostScriptTokenTypes.LBRACE)) {
var jumpLocation = this.operators.length;
this.operators.push(null, null);
var endOfTrue = this.operators.length;
this.parseBlock();
this.expect(PostScriptTokenTypes.RBRACE);
this.expect(PostScriptTokenTypes.IFELSE);
// The jump is added at the end of the true block to skip the false
// block.
this.operators[jumpLocation] = this.operators.length;
this.operators[jumpLocation + 1] = 'j';
this.operators[conditionLocation] = endOfTrue;
this.operators[conditionLocation + 1] = 'jz';
} else {
error('PS Function: error parsing conditional.');
}
}
};
return PostScriptParser;
})();
var PostScriptTokenTypes = {
LBRACE: 0,
RBRACE: 1,
NUMBER: 2,
OPERATOR: 3,
IF: 4,
IFELSE: 5
};
var PostScriptToken = (function PostScriptTokenClosure() {
function PostScriptToken(type, value) {
this.type = type;
this.value = value;
}
var opCache = {};
PostScriptToken.getOperator = function PostScriptToken_getOperator(op) {
var opValue = opCache[op];
if (opValue) {
return opValue;
}
return opCache[op] = new PostScriptToken(PostScriptTokenTypes.OPERATOR, op);
};
PostScriptToken.LBRACE = new PostScriptToken(PostScriptTokenTypes.LBRACE,
'{');
PostScriptToken.RBRACE = new PostScriptToken(PostScriptTokenTypes.RBRACE,
'}');
PostScriptToken.IF = new PostScriptToken(PostScriptTokenTypes.IF, 'IF');
PostScriptToken.IFELSE = new PostScriptToken(PostScriptTokenTypes.IFELSE,
'IFELSE');
return PostScriptToken;
})();
var PostScriptLexer = (function PostScriptLexerClosure() {
function PostScriptLexer(stream) {
this.stream = stream;
this.nextChar();
this.strBuf = [];
}
PostScriptLexer.prototype = {
nextChar: function PostScriptLexer_nextChar() {
return (this.currentChar = this.stream.getByte());
},
getToken: function PostScriptLexer_getToken() {
var comment = false;
var ch = this.currentChar;
// skip comments
while (true) {
if (ch < 0) {
return EOF;
}
if (comment) {
if (ch === 0x0A || ch === 0x0D) {
comment = false;
}
} else if (ch === 0x25) { // '%'
comment = true;
} else if (!Lexer.isSpace(ch)) {
break;
}
ch = this.nextChar();
}
switch (ch | 0) {
case 0x30: case 0x31: case 0x32: case 0x33: case 0x34: // '0'-'4'
case 0x35: case 0x36: case 0x37: case 0x38: case 0x39: // '5'-'9'
case 0x2B: case 0x2D: case 0x2E: // '+', '-', '.'
return new PostScriptToken(PostScriptTokenTypes.NUMBER,
this.getNumber());
case 0x7B: // '{'
this.nextChar();
return PostScriptToken.LBRACE;
case 0x7D: // '}'
this.nextChar();
return PostScriptToken.RBRACE;
}
// operator
var strBuf = this.strBuf;
strBuf.length = 0;
strBuf[0] = String.fromCharCode(ch);
while ((ch = this.nextChar()) >= 0 && // and 'A'-'Z', 'a'-'z'
((ch >= 0x41 && ch <= 0x5A) || (ch >= 0x61 && ch <= 0x7A))) {
strBuf.push(String.fromCharCode(ch));
}
var str = strBuf.join('');
switch (str.toLowerCase()) {
case 'if':
return PostScriptToken.IF;
case 'ifelse':
return PostScriptToken.IFELSE;
default:
return PostScriptToken.getOperator(str);
}
},
getNumber: function PostScriptLexer_getNumber() {
var ch = this.currentChar;
var strBuf = this.strBuf;
strBuf.length = 0;
strBuf[0] = String.fromCharCode(ch);
while ((ch = this.nextChar()) >= 0) {
if ((ch >= 0x30 && ch <= 0x39) || // '0'-'9'
ch === 0x2D || ch === 0x2E) { // '-', '.'
strBuf.push(String.fromCharCode(ch));
} else {
break;
}
}
var value = parseFloat(strBuf.join(''));
if (isNaN(value)) {
error('Invalid floating point number: ' + value);
}
return value;
}
};
return PostScriptLexer;
})();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,515 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals PDFJS, createPromiseCapability, LocalPdfManager, NetworkPdfManager,
NetworkManager, isInt, RANGE_CHUNK_SIZE, MissingPDFException,
UnexpectedResponseException, PasswordException, Promise, warn,
PasswordResponses, InvalidPDFException, UnknownErrorException,
XRefParseException, Ref, info, globalScope, error, MessageHandler */
'use strict';
var WorkerMessageHandler = PDFJS.WorkerMessageHandler = {
setup: function wphSetup(handler) {
var pdfManager;
function loadDocument(recoveryMode) {
var loadDocumentCapability = createPromiseCapability();
var parseSuccess = function parseSuccess() {
var numPagesPromise = pdfManager.ensureDoc('numPages');
var fingerprintPromise = pdfManager.ensureDoc('fingerprint');
var encryptedPromise = pdfManager.ensureXRef('encrypt');
Promise.all([numPagesPromise, fingerprintPromise,
encryptedPromise]).then(function onDocReady(results) {
var doc = {
numPages: results[0],
fingerprint: results[1],
encrypted: !!results[2],
};
loadDocumentCapability.resolve(doc);
},
parseFailure);
};
var parseFailure = function parseFailure(e) {
loadDocumentCapability.reject(e);
};
pdfManager.ensureDoc('checkHeader', []).then(function() {
pdfManager.ensureDoc('parseStartXRef', []).then(function() {
pdfManager.ensureDoc('parse', [recoveryMode]).then(
parseSuccess, parseFailure);
}, parseFailure);
}, parseFailure);
return loadDocumentCapability.promise;
}
function getPdfManager(data) {
var pdfManagerCapability = createPromiseCapability();
var source = data.source;
var disableRange = data.disableRange;
if (source.data) {
try {
pdfManager = new LocalPdfManager(source.data, source.password);
pdfManagerCapability.resolve();
} catch (ex) {
pdfManagerCapability.reject(ex);
}
return pdfManagerCapability.promise;
} else if (source.chunkedViewerLoading) {
try {
pdfManager = new NetworkPdfManager(source, handler);
pdfManagerCapability.resolve();
} catch (ex) {
pdfManagerCapability.reject(ex);
}
return pdfManagerCapability.promise;
}
var networkManager = new NetworkManager(source.url, {
httpHeaders: source.httpHeaders,
withCredentials: source.withCredentials
});
var cachedChunks = [];
var fullRequestXhrId = networkManager.requestFull({
onHeadersReceived: function onHeadersReceived() {
if (disableRange) {
return;
}
var fullRequestXhr = networkManager.getRequestXhr(fullRequestXhrId);
if (fullRequestXhr.getResponseHeader('Accept-Ranges') !== 'bytes') {
return;
}
var contentEncoding =
fullRequestXhr.getResponseHeader('Content-Encoding') || 'identity';
if (contentEncoding !== 'identity') {
return;
}
var length = fullRequestXhr.getResponseHeader('Content-Length');
length = parseInt(length, 10);
//#if (GENERIC || CHROME)
if (fullRequestXhr.status === 206) {
// Since Chrome 39, there exists a bug where cached responses are
// served with status code 206 for non-range requests.
// Content-Length does not specify the total size of the resource
// when the status code is 206 (see RFC 2616, section 14.16).
// In this case, extract the file size from the Content-Range
// header, which is defined to be "bytes start-end/length" for
// byte range requests.
// See https://github.com/mozilla/pdf.js/issues/5512 and
// https://code.google.com/p/chromium/issues/detail?id=442318
length = fullRequestXhr.getResponseHeader('Content-Range');
length = length && /bytes \d+-\d+\/(\d+)/.exec(length);
length = length && parseInt(length[1], 10);
}
//#endif
if (!isInt(length)) {
return;
}
source.length = length;
if (length <= 2 * RANGE_CHUNK_SIZE) {
// The file size is smaller than the size of two chunks, so it does
// not make any sense to abort the request and retry with a range
// request.
return;
}
if (networkManager.isStreamingRequest(fullRequestXhrId)) {
// We can continue fetching when progressive loading is enabled,
// and we don't need the autoFetch feature.
source.disableAutoFetch = true;
} else {
// NOTE: by cancelling the full request, and then issuing range
// requests, there will be an issue for sites where you can only
// request the pdf once. However, if this is the case, then the
// server should not be returning that it can support range
// requests.
networkManager.abortRequest(fullRequestXhrId);
}
try {
pdfManager = new NetworkPdfManager(source, handler);
pdfManagerCapability.resolve(pdfManager);
} catch (ex) {
pdfManagerCapability.reject(ex);
}
},
onProgressiveData: source.disableStream ? null :
function onProgressiveData(chunk) {
if (!pdfManager) {
cachedChunks.push(chunk);
return;
}
pdfManager.sendProgressiveData(chunk);
},
onDone: function onDone(args) {
if (pdfManager) {
return; // already processed
}
var pdfFile;
if (args === null) {
// TODO add some streaming manager, e.g. for unknown length files.
// The data was returned in the onProgressiveData, combining...
var pdfFileLength = 0, pos = 0;
cachedChunks.forEach(function (chunk) {
pdfFileLength += chunk.byteLength;
});
if (source.length && pdfFileLength !== source.length) {
warn('reported HTTP length is different from actual');
}
var pdfFileArray = new Uint8Array(pdfFileLength);
cachedChunks.forEach(function (chunk) {
pdfFileArray.set(new Uint8Array(chunk), pos);
pos += chunk.byteLength;
});
pdfFile = pdfFileArray.buffer;
} else {
pdfFile = args.chunk;
}
// the data is array, instantiating directly from it
try {
pdfManager = new LocalPdfManager(pdfFile, source.password);
pdfManagerCapability.resolve();
} catch (ex) {
pdfManagerCapability.reject(ex);
}
},
onError: function onError(status) {
var exception;
if (status === 404) {
exception = new MissingPDFException('Missing PDF "' +
source.url + '".');
handler.send('MissingPDF', exception);
} else {
exception = new UnexpectedResponseException(
'Unexpected server response (' + status +
') while retrieving PDF "' + source.url + '".', status);
handler.send('UnexpectedResponse', exception);
}
},
onProgress: function onProgress(evt) {
handler.send('DocProgress', {
loaded: evt.loaded,
total: evt.lengthComputable ? evt.total : source.length
});
}
});
return pdfManagerCapability.promise;
}
handler.on('test', function wphSetupTest(data) {
// check if Uint8Array can be sent to worker
if (!(data instanceof Uint8Array)) {
handler.send('test', false);
return;
}
// making sure postMessage transfers are working
var supportTransfers = data[0] === 255;
handler.postMessageTransfers = supportTransfers;
// check if the response property is supported by xhr
var xhr = new XMLHttpRequest();
var responseExists = 'response' in xhr;
// check if the property is actually implemented
try {
var dummy = xhr.responseType;
} catch (e) {
responseExists = false;
}
if (!responseExists) {
handler.send('test', false);
return;
}
handler.send('test', {
supportTypedArray: true,
supportTransfers: supportTransfers
});
});
handler.on('GetDocRequest', function wphSetupDoc(data) {
var onSuccess = function(doc) {
handler.send('GetDoc', { pdfInfo: doc });
};
var onFailure = function(e) {
if (e instanceof PasswordException) {
if (e.code === PasswordResponses.NEED_PASSWORD) {
handler.send('NeedPassword', e);
} else if (e.code === PasswordResponses.INCORRECT_PASSWORD) {
handler.send('IncorrectPassword', e);
}
} else if (e instanceof InvalidPDFException) {
handler.send('InvalidPDF', e);
} else if (e instanceof MissingPDFException) {
handler.send('MissingPDF', e);
} else if (e instanceof UnexpectedResponseException) {
handler.send('UnexpectedResponse', e);
} else {
handler.send('UnknownError',
new UnknownErrorException(e.message, e.toString()));
}
};
PDFJS.maxImageSize = data.maxImageSize === undefined ?
-1 : data.maxImageSize;
PDFJS.disableFontFace = data.disableFontFace;
PDFJS.disableCreateObjectURL = data.disableCreateObjectURL;
PDFJS.verbosity = data.verbosity;
PDFJS.cMapUrl = data.cMapUrl === undefined ?
null : data.cMapUrl;
PDFJS.cMapPacked = data.cMapPacked === true;
getPdfManager(data).then(function () {
handler.send('PDFManagerReady', null);
pdfManager.onLoadedStream().then(function(stream) {
handler.send('DataLoaded', { length: stream.bytes.byteLength });
});
}).then(function pdfManagerReady() {
loadDocument(false).then(onSuccess, function loadFailure(ex) {
// Try again with recoveryMode == true
if (!(ex instanceof XRefParseException)) {
if (ex instanceof PasswordException) {
// after password exception prepare to receive a new password
// to repeat loading
pdfManager.passwordChanged().then(pdfManagerReady);
}
onFailure(ex);
return;
}
pdfManager.requestLoadedStream();
pdfManager.onLoadedStream().then(function() {
loadDocument(true).then(onSuccess, onFailure);
});
}, onFailure);
}, onFailure);
});
handler.on('GetPage', function wphSetupGetPage(data) {
return pdfManager.getPage(data.pageIndex).then(function(page) {
var rotatePromise = pdfManager.ensure(page, 'rotate');
var refPromise = pdfManager.ensure(page, 'ref');
var viewPromise = pdfManager.ensure(page, 'view');
return Promise.all([rotatePromise, refPromise, viewPromise]).then(
function(results) {
return {
rotate: results[0],
ref: results[1],
view: results[2]
};
});
});
});
handler.on('GetPageIndex', function wphSetupGetPageIndex(data) {
var ref = new Ref(data.ref.num, data.ref.gen);
var catalog = pdfManager.pdfDocument.catalog;
return catalog.getPageIndex(ref);
});
handler.on('GetDestinations',
function wphSetupGetDestinations(data) {
return pdfManager.ensureCatalog('destinations');
}
);
handler.on('GetDestination',
function wphSetupGetDestination(data) {
return pdfManager.ensureCatalog('getDestination', [ data.id ]);
}
);
handler.on('GetAttachments',
function wphSetupGetAttachments(data) {
return pdfManager.ensureCatalog('attachments');
}
);
handler.on('GetJavaScript',
function wphSetupGetJavaScript(data) {
return pdfManager.ensureCatalog('javaScript');
}
);
handler.on('GetOutline',
function wphSetupGetOutline(data) {
return pdfManager.ensureCatalog('documentOutline');
}
);
handler.on('GetMetadata',
function wphSetupGetMetadata(data) {
return Promise.all([pdfManager.ensureDoc('documentInfo'),
pdfManager.ensureCatalog('metadata')]);
}
);
handler.on('GetData', function wphSetupGetData(data) {
pdfManager.requestLoadedStream();
return pdfManager.onLoadedStream().then(function(stream) {
return stream.bytes;
});
});
handler.on('GetStats',
function wphSetupGetStats(data) {
return pdfManager.pdfDocument.xref.stats;
}
);
handler.on('UpdatePassword', function wphSetupUpdatePassword(data) {
pdfManager.updatePassword(data);
});
handler.on('GetAnnotations', function wphSetupGetAnnotations(data) {
return pdfManager.getPage(data.pageIndex).then(function(page) {
return pdfManager.ensure(page, 'getAnnotationsData', []);
});
});
handler.on('RenderPageRequest', function wphSetupRenderPage(data) {
pdfManager.getPage(data.pageIndex).then(function(page) {
var pageNum = data.pageIndex + 1;
var start = Date.now();
// Pre compile the pdf page and fetch the fonts/images.
page.getOperatorList(handler, data.intent).then(function(operatorList) {
info('page=' + pageNum + ' - getOperatorList: time=' +
(Date.now() - start) + 'ms, len=' + operatorList.fnArray.length);
}, function(e) {
var minimumStackMessage =
'worker.js: while trying to getPage() and getOperatorList()';
var wrappedException;
// Turn the error into an obj that can be serialized
if (typeof e === 'string') {
wrappedException = {
message: e,
stack: minimumStackMessage
};
} else if (typeof e === 'object') {
wrappedException = {
message: e.message || e.toString(),
stack: e.stack || minimumStackMessage
};
} else {
wrappedException = {
message: 'Unknown exception type: ' + (typeof e),
stack: minimumStackMessage
};
}
handler.send('PageError', {
pageNum: pageNum,
error: wrappedException,
intent: data.intent
});
});
});
}, this);
handler.on('GetTextContent', function wphExtractText(data) {
return pdfManager.getPage(data.pageIndex).then(function(page) {
var pageNum = data.pageIndex + 1;
var start = Date.now();
return page.extractTextContent().then(function(textContent) {
info('text indexing: page=' + pageNum + ' - time=' +
(Date.now() - start) + 'ms');
return textContent;
});
});
});
handler.on('Cleanup', function wphCleanup(data) {
return pdfManager.cleanup();
});
handler.on('Terminate', function wphTerminate(data) {
pdfManager.terminate();
});
}
};
var consoleTimer = {};
var workerConsole = {
log: function log() {
var args = Array.prototype.slice.call(arguments);
globalScope.postMessage({
action: 'console_log',
data: args
});
},
error: function error() {
var args = Array.prototype.slice.call(arguments);
globalScope.postMessage({
action: 'console_error',
data: args
});
throw 'pdf.js execution error';
},
time: function time(name) {
consoleTimer[name] = Date.now();
},
timeEnd: function timeEnd(name) {
var time = consoleTimer[name];
if (!time) {
error('Unknown timer name ' + name);
}
this.log('Timer:', name, Date.now() - time);
}
};
// Worker thread?
if (typeof window === 'undefined') {
if (!('console' in globalScope)) {
globalScope.console = workerConsole;
}
// Listen for unsupported features so we can pass them on to the main thread.
PDFJS.UnsupportedManager.listen(function (msg) {
globalScope.postMessage({
action: '_unsupported_feature',
data: msg
});
});
var handler = new MessageHandler('worker_processor', this);
WorkerMessageHandler.setup(handler);
}
@@ -0,0 +1,243 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2014 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals PDFJS, Util, AnnotationType */
'use strict';
var ANNOT_MIN_SIZE = 10; // px
var AnnotationUtils = (function AnnotationUtilsClosure() {
// TODO(mack): This dupes some of the logic in CanvasGraphics.setFont()
function setTextStyles(element, item, fontObj) {
var style = element.style;
style.fontSize = item.fontSize + 'px';
style.direction = item.fontDirection < 0 ? 'rtl': 'ltr';
if (!fontObj) {
return;
}
style.fontWeight = fontObj.black ?
(fontObj.bold ? 'bolder' : 'bold') :
(fontObj.bold ? 'bold' : 'normal');
style.fontStyle = fontObj.italic ? 'italic' : 'normal';
var fontName = fontObj.loadedName;
var fontFamily = fontName ? '"' + fontName + '", ' : '';
// Use a reasonable default font if the font doesn't specify a fallback
var fallbackName = fontObj.fallbackName || 'Helvetica, sans-serif';
style.fontFamily = fontFamily + fallbackName;
}
function initContainer(item, drawBorder) {
var container = document.createElement('section');
var cstyle = container.style;
var width = item.rect[2] - item.rect[0];
var height = item.rect[3] - item.rect[1];
var bWidth = item.borderWidth || 0;
if (bWidth) {
width = width - 2 * bWidth;
height = height - 2 * bWidth;
cstyle.borderWidth = bWidth + 'px';
var color = item.color;
if (drawBorder && color) {
cstyle.borderStyle = 'solid';
cstyle.borderColor = Util.makeCssRgb(Math.round(color[0] * 255),
Math.round(color[1] * 255),
Math.round(color[2] * 255));
}
}
cstyle.width = width + 'px';
cstyle.height = height + 'px';
return container;
}
function getHtmlElementForTextWidgetAnnotation(item, commonObjs) {
var element = document.createElement('div');
var width = item.rect[2] - item.rect[0];
var height = item.rect[3] - item.rect[1];
element.style.width = width + 'px';
element.style.height = height + 'px';
element.style.display = 'table';
var content = document.createElement('div');
content.textContent = item.fieldValue;
var textAlignment = item.textAlignment;
content.style.textAlign = ['left', 'center', 'right'][textAlignment];
content.style.verticalAlign = 'middle';
content.style.display = 'table-cell';
var fontObj = item.fontRefName ?
commonObjs.getData(item.fontRefName) : null;
setTextStyles(content, item, fontObj);
element.appendChild(content);
return element;
}
function getHtmlElementForTextAnnotation(item) {
var rect = item.rect;
// sanity check because of OOo-generated PDFs
if ((rect[3] - rect[1]) < ANNOT_MIN_SIZE) {
rect[3] = rect[1] + ANNOT_MIN_SIZE;
}
if ((rect[2] - rect[0]) < ANNOT_MIN_SIZE) {
rect[2] = rect[0] + (rect[3] - rect[1]); // make it square
}
var container = initContainer(item, false);
container.className = 'annotText';
var image = document.createElement('img');
image.style.height = container.style.height;
image.style.width = container.style.width;
var iconName = item.name;
image.src = PDFJS.imageResourcesPath + 'annotation-' +
iconName.toLowerCase() + '.svg';
image.alt = '[{{type}} Annotation]';
image.dataset.l10nId = 'text_annotation_type';
image.dataset.l10nArgs = JSON.stringify({type: iconName});
var contentWrapper = document.createElement('div');
contentWrapper.className = 'annotTextContentWrapper';
contentWrapper.style.left = Math.floor(rect[2] - rect[0] + 5) + 'px';
contentWrapper.style.top = '-10px';
var content = document.createElement('div');
content.className = 'annotTextContent';
content.setAttribute('hidden', true);
var i, ii;
if (item.hasBgColor) {
var color = item.color;
// Enlighten the color (70%)
var BACKGROUND_ENLIGHT = 0.7;
var r = BACKGROUND_ENLIGHT * (1.0 - color[0]) + color[0];
var g = BACKGROUND_ENLIGHT * (1.0 - color[1]) + color[1];
var b = BACKGROUND_ENLIGHT * (1.0 - color[2]) + color[2];
content.style.backgroundColor = Util.makeCssRgb((r * 255) | 0,
(g * 255) | 0,
(b * 255) | 0);
}
var title = document.createElement('h1');
var text = document.createElement('p');
title.textContent = item.title;
if (!item.content && !item.title) {
content.setAttribute('hidden', true);
} else {
var e = document.createElement('span');
var lines = item.content.split(/(?:\r\n?|\n)/);
for (i = 0, ii = lines.length; i < ii; ++i) {
var line = lines[i];
e.appendChild(document.createTextNode(line));
if (i < (ii - 1)) {
e.appendChild(document.createElement('br'));
}
}
text.appendChild(e);
var pinned = false;
var showAnnotation = function showAnnotation(pin) {
if (pin) {
pinned = true;
}
if (content.hasAttribute('hidden')) {
container.style.zIndex += 1;
content.removeAttribute('hidden');
}
};
var hideAnnotation = function hideAnnotation(unpin) {
if (unpin) {
pinned = false;
}
if (!content.hasAttribute('hidden') && !pinned) {
container.style.zIndex -= 1;
content.setAttribute('hidden', true);
}
};
var toggleAnnotation = function toggleAnnotation() {
if (pinned) {
hideAnnotation(true);
} else {
showAnnotation(true);
}
};
image.addEventListener('click', function image_clickHandler() {
toggleAnnotation();
}, false);
image.addEventListener('mouseover', function image_mouseOverHandler() {
showAnnotation();
}, false);
image.addEventListener('mouseout', function image_mouseOutHandler() {
hideAnnotation();
}, false);
content.addEventListener('click', function content_clickHandler() {
hideAnnotation(true);
}, false);
}
content.appendChild(title);
content.appendChild(text);
contentWrapper.appendChild(content);
container.appendChild(image);
container.appendChild(contentWrapper);
return container;
}
function getHtmlElementForLinkAnnotation(item) {
var container = initContainer(item, true);
container.className = 'annotLink';
var link = document.createElement('a');
link.href = link.title = item.url || '';
container.appendChild(link);
return container;
}
function getHtmlElement(data, objs) {
switch (data.annotationType) {
case AnnotationType.WIDGET:
return getHtmlElementForTextWidgetAnnotation(data, objs);
case AnnotationType.TEXT:
return getHtmlElementForTextAnnotation(data);
case AnnotationType.LINK:
return getHtmlElementForLinkAnnotation(data);
default:
throw new Error('Unsupported annotationType: ' + data.annotationType);
}
}
return {
getHtmlElement: getHtmlElement
};
})();
PDFJS.AnnotationUtils = AnnotationUtils;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,375 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals PDFJS, shadow, isWorker, assert, warn, bytesToString, string32,
globalScope, FontFace, Promise */
'use strict';
PDFJS.disableFontFace = false;
var FontLoader = {
insertRule: function fontLoaderInsertRule(rule) {
var styleElement = document.getElementById('PDFJS_FONT_STYLE_TAG');
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'PDFJS_FONT_STYLE_TAG';
document.documentElement.getElementsByTagName('head')[0].appendChild(
styleElement);
}
var styleSheet = styleElement.sheet;
styleSheet.insertRule(rule, styleSheet.cssRules.length);
},
clear: function fontLoaderClear() {
var styleElement = document.getElementById('PDFJS_FONT_STYLE_TAG');
if (styleElement) {
styleElement.parentNode.removeChild(styleElement);
}
//#if !(MOZCENTRAL)
this.nativeFontFaces.forEach(function(nativeFontFace) {
document.fonts.delete(nativeFontFace);
});
this.nativeFontFaces.length = 0;
//#endif
},
//#if !(MOZCENTRAL)
get loadTestFont() {
// This is a CFF font with 1 glyph for '.' that fills its entire width and
// height.
return shadow(this, 'loadTestFont', atob(
'T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQAFQ' +
'AABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAAALwA' +
'AAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgAAAAGbm' +
'FtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1AAsD6AAA' +
'AADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD6AAAAAAD6A' +
'ABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACMAooCvAAAAeAA' +
'MQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4DIP84AFoDIQAAAA' +
'AAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAAAAEAAQAAAAEAAAAA' +
'AAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUAAQAAAAEAAAAAAAYAAQ' +
'AAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgABAAMAAQQJAAMAAgABAAMA' +
'AQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABYAAAAAAAAAwAAAAMAAAAcAA' +
'EAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAAAC7////TAAEAAAAAAAABBgAA' +
'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAA' +
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAA' +
'AAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAAAAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgc' +
'A/gXBIwMAYuL+nz5tQXkD5j3CBLnEQACAQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWF' +
'hYWFhYWFhYAAABAQAADwACAQEEE/t3Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQA' +
'AAAAAAABAAAAAMmJbzEAAAAAzgTjFQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAg' +
'ABAAAAAAAAAAAD6AAAAAAAAA=='
));
},
loadTestFontId: 0,
loadingContext: {
requests: [],
nextRequestId: 0
},
isSyncFontLoadingSupported: (function detectSyncFontLoadingSupport() {
if (isWorker) {
return false;
}
// User agent string sniffing is bad, but there is no reliable way to tell
// if font is fully loaded and ready to be used with canvas.
var userAgent = window.navigator.userAgent;
var m = /Mozilla\/5.0.*?rv:(\d+).*? Gecko/.exec(userAgent);
if (m && m[1] >= 14) {
return true;
}
// TODO other browsers
if (userAgent === 'node') {
return true;
}
return false;
})(),
nativeFontFaces: [],
isFontLoadingAPISupported: !isWorker && !!document.fonts,
addNativeFontFace: function fontLoader_addNativeFontFace(nativeFontFace) {
this.nativeFontFaces.push(nativeFontFace);
document.fonts.add(nativeFontFace);
},
bind: function fontLoaderBind(fonts, callback) {
assert(!isWorker, 'bind() shall be called from main thread');
var rules = [];
var fontsToLoad = [];
var fontLoadPromises = [];
for (var i = 0, ii = fonts.length; i < ii; i++) {
var font = fonts[i];
// Add the font to the DOM only once or skip if the font
// is already loaded.
if (font.attached || font.loading === false) {
continue;
}
font.attached = true;
if (this.isFontLoadingAPISupported) {
var nativeFontFace = font.createNativeFontFace();
if (nativeFontFace) {
fontLoadPromises.push(nativeFontFace.loaded);
}
} else {
var rule = font.bindDOM();
if (rule) {
rules.push(rule);
fontsToLoad.push(font);
}
}
}
var request = FontLoader.queueLoadingCallback(callback);
if (this.isFontLoadingAPISupported) {
Promise.all(fontsToLoad).then(function() {
request.complete();
});
} else if (rules.length > 0 && !this.isSyncFontLoadingSupported) {
FontLoader.prepareFontLoadEvent(rules, fontsToLoad, request);
} else {
request.complete();
}
},
queueLoadingCallback: function FontLoader_queueLoadingCallback(callback) {
function LoadLoader_completeRequest() {
assert(!request.end, 'completeRequest() cannot be called twice');
request.end = Date.now();
// sending all completed requests in order how they were queued
while (context.requests.length > 0 && context.requests[0].end) {
var otherRequest = context.requests.shift();
setTimeout(otherRequest.callback, 0);
}
}
var context = FontLoader.loadingContext;
var requestId = 'pdfjs-font-loading-' + (context.nextRequestId++);
var request = {
id: requestId,
complete: LoadLoader_completeRequest,
callback: callback,
started: Date.now()
};
context.requests.push(request);
return request;
},
prepareFontLoadEvent: function fontLoaderPrepareFontLoadEvent(rules,
fonts,
request) {
/** Hack begin */
// There's currently no event when a font has finished downloading so the
// following code is a dirty hack to 'guess' when a font is
// ready. It's assumed fonts are loaded in order, so add a known test
// font after the desired fonts and then test for the loading of that
// test font.
function int32(data, offset) {
return (data.charCodeAt(offset) << 24) |
(data.charCodeAt(offset + 1) << 16) |
(data.charCodeAt(offset + 2) << 8) |
(data.charCodeAt(offset + 3) & 0xff);
}
function spliceString(s, offset, remove, insert) {
var chunk1 = s.substr(0, offset);
var chunk2 = s.substr(offset + remove);
return chunk1 + insert + chunk2;
}
var i, ii;
var canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
var ctx = canvas.getContext('2d');
var called = 0;
function isFontReady(name, callback) {
called++;
// With setTimeout clamping this gives the font ~100ms to load.
if(called > 30) {
warn('Load test font never loaded.');
callback();
return;
}
ctx.font = '30px ' + name;
ctx.fillText('.', 0, 20);
var imageData = ctx.getImageData(0, 0, 1, 1);
if (imageData.data[3] > 0) {
callback();
return;
}
setTimeout(isFontReady.bind(null, name, callback));
}
var loadTestFontId = 'lt' + Date.now() + this.loadTestFontId++;
// Chromium seems to cache fonts based on a hash of the actual font data,
// so the font must be modified for each load test else it will appear to
// be loaded already.
// TODO: This could maybe be made faster by avoiding the btoa of the full
// font by splitting it in chunks before hand and padding the font id.
var data = this.loadTestFont;
var COMMENT_OFFSET = 976; // has to be on 4 byte boundary (for checksum)
data = spliceString(data, COMMENT_OFFSET, loadTestFontId.length,
loadTestFontId);
// CFF checksum is important for IE, adjusting it
var CFF_CHECKSUM_OFFSET = 16;
var XXXX_VALUE = 0x58585858; // the "comment" filled with 'X'
var checksum = int32(data, CFF_CHECKSUM_OFFSET);
for (i = 0, ii = loadTestFontId.length - 3; i < ii; i += 4) {
checksum = (checksum - XXXX_VALUE + int32(loadTestFontId, i)) | 0;
}
if (i < loadTestFontId.length) { // align to 4 bytes boundary
checksum = (checksum - XXXX_VALUE +
int32(loadTestFontId + 'XXX', i)) | 0;
}
data = spliceString(data, CFF_CHECKSUM_OFFSET, 4, string32(checksum));
var url = 'url(data:font/opentype;base64,' + btoa(data) + ');';
var rule = '@font-face { font-family:"' + loadTestFontId + '";src:' +
url + '}';
FontLoader.insertRule(rule);
var names = [];
for (i = 0, ii = fonts.length; i < ii; i++) {
names.push(fonts[i].loadedName);
}
names.push(loadTestFontId);
var div = document.createElement('div');
div.setAttribute('style',
'visibility: hidden;' +
'width: 10px; height: 10px;' +
'position: absolute; top: 0px; left: 0px;');
for (i = 0, ii = names.length; i < ii; ++i) {
var span = document.createElement('span');
span.textContent = 'Hi';
span.style.fontFamily = names[i];
div.appendChild(span);
}
document.body.appendChild(div);
isFontReady(loadTestFontId, function() {
document.body.removeChild(div);
request.complete();
});
/** Hack end */
}
//#else
//bind: function fontLoaderBind(fonts, callback) {
// assert(!isWorker, 'bind() shall be called from main thread');
//
// for (var i = 0, ii = fonts.length; i < ii; i++) {
// var font = fonts[i];
// if (font.attached) {
// continue;
// }
//
// font.attached = true;
// font.bindDOM()
// }
//
// setTimeout(callback);
//}
//#endif
};
var FontFaceObject = (function FontFaceObjectClosure() {
function FontFaceObject(name, file, properties) {
this.compiledGlyphs = {};
if (arguments.length === 1) {
// importing translated data
var data = arguments[0];
for (var i in data) {
this[i] = data[i];
}
return;
}
}
FontFaceObject.prototype = {
//#if !(MOZCENTRAL)
createNativeFontFace: function FontFaceObject_createNativeFontFace() {
if (!this.data) {
return null;
}
if (PDFJS.disableFontFace) {
this.disableFontFace = true;
return null;
}
var nativeFontFace = new FontFace(this.loadedName, this.data, {});
FontLoader.addNativeFontFace(nativeFontFace);
if (PDFJS.pdfBug && 'FontInspector' in globalScope &&
globalScope['FontInspector'].enabled) {
globalScope['FontInspector'].fontAdded(this);
}
return nativeFontFace;
},
//#endif
bindDOM: function FontFaceObject_bindDOM() {
if (!this.data) {
return null;
}
if (PDFJS.disableFontFace) {
this.disableFontFace = true;
return null;
}
var data = bytesToString(new Uint8Array(this.data));
var fontName = this.loadedName;
// Add the font-face rule to the document
var url = ('url(data:' + this.mimetype + ';base64,' +
window.btoa(data) + ');');
var rule = '@font-face { font-family:"' + fontName + '";src:' + url + '}';
FontLoader.insertRule(rule);
if (PDFJS.pdfBug && 'FontInspector' in globalScope &&
globalScope['FontInspector'].enabled) {
globalScope['FontInspector'].fontAdded(this, url);
}
return rule;
},
getPathGenerator: function FontLoader_getPathGenerator(objs, character) {
if (!(character in this.compiledGlyphs)) {
var js = objs.get(this.loadedName + '_path_' + character);
/*jshint -W054 */
this.compiledGlyphs[character] = new Function('c', 'size', js);
}
return this.compiledGlyphs[character];
}
};
return FontFaceObject;
})();
@@ -0,0 +1,99 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* globals Document, error, PDFJS */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var Metadata = PDFJS.Metadata = (function MetadataClosure() {
function fixMetadata(meta) {
return meta.replace(/>\\376\\377([^<]+)/g, function(all, codes) {
var bytes = codes.replace(/\\([0-3])([0-7])([0-7])/g,
function(code, d1, d2, d3) {
return String.fromCharCode(d1 * 64 + d2 * 8 + d3 * 1);
});
var chars = '';
for (var i = 0; i < bytes.length; i += 2) {
var code = bytes.charCodeAt(i) * 256 + bytes.charCodeAt(i + 1);
chars += code >= 32 && code < 127 && code !== 60 && code !== 62 &&
code !== 38 && false ? String.fromCharCode(code) :
'&#x' + (0x10000 + code).toString(16).substring(1) + ';';
}
return '>' + chars;
});
}
function Metadata(meta) {
if (typeof meta === 'string') {
// Ghostscript produces invalid metadata
meta = fixMetadata(meta);
var parser = new DOMParser();
meta = parser.parseFromString(meta, 'application/xml');
} else if (!(meta instanceof Document)) {
error('Metadata: Invalid metadata object');
}
this.metaDocument = meta;
this.metadata = {};
this.parse();
}
Metadata.prototype = {
parse: function Metadata_parse() {
var doc = this.metaDocument;
var rdf = doc.documentElement;
if (rdf.nodeName.toLowerCase() !== 'rdf:rdf') { // Wrapped in <xmpmeta>
rdf = rdf.firstChild;
while (rdf && rdf.nodeName.toLowerCase() !== 'rdf:rdf') {
rdf = rdf.nextSibling;
}
}
var nodeName = (rdf) ? rdf.nodeName.toLowerCase() : null;
if (!rdf || nodeName !== 'rdf:rdf' || !rdf.hasChildNodes()) {
return;
}
var children = rdf.childNodes, desc, entry, name, i, ii, length, iLength;
for (i = 0, length = children.length; i < length; i++) {
desc = children[i];
if (desc.nodeName.toLowerCase() !== 'rdf:description') {
continue;
}
for (ii = 0, iLength = desc.childNodes.length; ii < iLength; ii++) {
if (desc.childNodes[ii].nodeName.toLowerCase() !== '#text') {
entry = desc.childNodes[ii];
name = entry.nodeName.toLowerCase();
this.metadata[name] = entry.textContent.trim();
}
}
}
},
get: function Metadata_get(name) {
return this.metadata[name] || null;
},
has: function Metadata_has(name) {
return typeof this.metadata[name] !== 'undefined';
}
};
return Metadata;
})();
@@ -0,0 +1,425 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2014 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals CanvasGraphics, CachedCanvases, ColorSpace, Util, error, info,
isArray, makeCssRgb, WebGLUtils */
'use strict';
var ShadingIRs = {};
ShadingIRs.RadialAxial = {
fromIR: function RadialAxial_fromIR(raw) {
var type = raw[1];
var colorStops = raw[2];
var p0 = raw[3];
var p1 = raw[4];
var r0 = raw[5];
var r1 = raw[6];
return {
type: 'Pattern',
getPattern: function RadialAxial_getPattern(ctx) {
var grad;
if (type === 'axial') {
grad = ctx.createLinearGradient(p0[0], p0[1], p1[0], p1[1]);
} else if (type === 'radial') {
grad = ctx.createRadialGradient(p0[0], p0[1], r0, p1[0], p1[1], r1);
}
for (var i = 0, ii = colorStops.length; i < ii; ++i) {
var c = colorStops[i];
grad.addColorStop(c[0], c[1]);
}
return grad;
}
};
}
};
var createMeshCanvas = (function createMeshCanvasClosure() {
function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) {
// Very basic Gouraud-shaded triangle rasterization algorithm.
var coords = context.coords, colors = context.colors;
var bytes = data.data, rowSize = data.width * 4;
var tmp;
if (coords[p1 + 1] > coords[p2 + 1]) {
tmp = p1; p1 = p2; p2 = tmp; tmp = c1; c1 = c2; c2 = tmp;
}
if (coords[p2 + 1] > coords[p3 + 1]) {
tmp = p2; p2 = p3; p3 = tmp; tmp = c2; c2 = c3; c3 = tmp;
}
if (coords[p1 + 1] > coords[p2 + 1]) {
tmp = p1; p1 = p2; p2 = tmp; tmp = c1; c1 = c2; c2 = tmp;
}
var x1 = (coords[p1] + context.offsetX) * context.scaleX;
var y1 = (coords[p1 + 1] + context.offsetY) * context.scaleY;
var x2 = (coords[p2] + context.offsetX) * context.scaleX;
var y2 = (coords[p2 + 1] + context.offsetY) * context.scaleY;
var x3 = (coords[p3] + context.offsetX) * context.scaleX;
var y3 = (coords[p3 + 1] + context.offsetY) * context.scaleY;
if (y1 >= y3) {
return;
}
var c1r = colors[c1], c1g = colors[c1 + 1], c1b = colors[c1 + 2];
var c2r = colors[c2], c2g = colors[c2 + 1], c2b = colors[c2 + 2];
var c3r = colors[c3], c3g = colors[c3 + 1], c3b = colors[c3 + 2];
var minY = Math.round(y1), maxY = Math.round(y3);
var xa, car, cag, cab;
var xb, cbr, cbg, cbb;
var k;
for (var y = minY; y <= maxY; y++) {
if (y < y2) {
k = y < y1 ? 0 : y1 === y2 ? 1 : (y1 - y) / (y1 - y2);
xa = x1 - (x1 - x2) * k;
car = c1r - (c1r - c2r) * k;
cag = c1g - (c1g - c2g) * k;
cab = c1b - (c1b - c2b) * k;
} else {
k = y > y3 ? 1 : y2 === y3 ? 0 : (y2 - y) / (y2 - y3);
xa = x2 - (x2 - x3) * k;
car = c2r - (c2r - c3r) * k;
cag = c2g - (c2g - c3g) * k;
cab = c2b - (c2b - c3b) * k;
}
k = y < y1 ? 0 : y > y3 ? 1 : (y1 - y) / (y1 - y3);
xb = x1 - (x1 - x3) * k;
cbr = c1r - (c1r - c3r) * k;
cbg = c1g - (c1g - c3g) * k;
cbb = c1b - (c1b - c3b) * k;
var x1_ = Math.round(Math.min(xa, xb));
var x2_ = Math.round(Math.max(xa, xb));
var j = rowSize * y + x1_ * 4;
for (var x = x1_; x <= x2_; x++) {
k = (xa - x) / (xa - xb);
k = k < 0 ? 0 : k > 1 ? 1 : k;
bytes[j++] = (car - (car - cbr) * k) | 0;
bytes[j++] = (cag - (cag - cbg) * k) | 0;
bytes[j++] = (cab - (cab - cbb) * k) | 0;
bytes[j++] = 255;
}
}
}
function drawFigure(data, figure, context) {
var ps = figure.coords;
var cs = figure.colors;
var i, ii;
switch (figure.type) {
case 'lattice':
var verticesPerRow = figure.verticesPerRow;
var rows = Math.floor(ps.length / verticesPerRow) - 1;
var cols = verticesPerRow - 1;
for (i = 0; i < rows; i++) {
var q = i * verticesPerRow;
for (var j = 0; j < cols; j++, q++) {
drawTriangle(data, context,
ps[q], ps[q + 1], ps[q + verticesPerRow],
cs[q], cs[q + 1], cs[q + verticesPerRow]);
drawTriangle(data, context,
ps[q + verticesPerRow + 1], ps[q + 1], ps[q + verticesPerRow],
cs[q + verticesPerRow + 1], cs[q + 1], cs[q + verticesPerRow]);
}
}
break;
case 'triangles':
for (i = 0, ii = ps.length; i < ii; i += 3) {
drawTriangle(data, context,
ps[i], ps[i + 1], ps[i + 2],
cs[i], cs[i + 1], cs[i + 2]);
}
break;
default:
error('illigal figure');
break;
}
}
function createMeshCanvas(bounds, combinesScale, coords, colors, figures,
backgroundColor) {
// we will increase scale on some weird factor to let antialiasing take
// care of "rough" edges
var EXPECTED_SCALE = 1.1;
// MAX_PATTERN_SIZE is used to avoid OOM situation.
var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough
var offsetX = Math.floor(bounds[0]);
var offsetY = Math.floor(bounds[1]);
var boundsWidth = Math.ceil(bounds[2]) - offsetX;
var boundsHeight = Math.ceil(bounds[3]) - offsetY;
var width = Math.min(Math.ceil(Math.abs(boundsWidth * combinesScale[0] *
EXPECTED_SCALE)), MAX_PATTERN_SIZE);
var height = Math.min(Math.ceil(Math.abs(boundsHeight * combinesScale[1] *
EXPECTED_SCALE)), MAX_PATTERN_SIZE);
var scaleX = boundsWidth / width;
var scaleY = boundsHeight / height;
var context = {
coords: coords,
colors: colors,
offsetX: -offsetX,
offsetY: -offsetY,
scaleX: 1 / scaleX,
scaleY: 1 / scaleY
};
var canvas, tmpCanvas, i, ii;
if (WebGLUtils.isEnabled) {
canvas = WebGLUtils.drawFigures(width, height, backgroundColor,
figures, context);
// https://bugzilla.mozilla.org/show_bug.cgi?id=972126
tmpCanvas = CachedCanvases.getCanvas('mesh', width, height, false);
tmpCanvas.context.drawImage(canvas, 0, 0);
canvas = tmpCanvas.canvas;
} else {
tmpCanvas = CachedCanvases.getCanvas('mesh', width, height, false);
var tmpCtx = tmpCanvas.context;
var data = tmpCtx.createImageData(width, height);
if (backgroundColor) {
var bytes = data.data;
for (i = 0, ii = bytes.length; i < ii; i += 4) {
bytes[i] = backgroundColor[0];
bytes[i + 1] = backgroundColor[1];
bytes[i + 2] = backgroundColor[2];
bytes[i + 3] = 255;
}
}
for (i = 0; i < figures.length; i++) {
drawFigure(data, figures[i], context);
}
tmpCtx.putImageData(data, 0, 0);
canvas = tmpCanvas.canvas;
}
return {canvas: canvas, offsetX: offsetX, offsetY: offsetY,
scaleX: scaleX, scaleY: scaleY};
}
return createMeshCanvas;
})();
ShadingIRs.Mesh = {
fromIR: function Mesh_fromIR(raw) {
//var type = raw[1];
var coords = raw[2];
var colors = raw[3];
var figures = raw[4];
var bounds = raw[5];
var matrix = raw[6];
//var bbox = raw[7];
var background = raw[8];
return {
type: 'Pattern',
getPattern: function Mesh_getPattern(ctx, owner, shadingFill) {
var scale;
if (shadingFill) {
scale = Util.singularValueDecompose2dScale(ctx.mozCurrentTransform);
} else {
// Obtain scale from matrix and current transformation matrix.
scale = Util.singularValueDecompose2dScale(owner.baseTransform);
if (matrix) {
var matrixScale = Util.singularValueDecompose2dScale(matrix);
scale = [scale[0] * matrixScale[0],
scale[1] * matrixScale[1]];
}
}
// Rasterizing on the main thread since sending/queue large canvases
// might cause OOM.
var temporaryPatternCanvas = createMeshCanvas(bounds, scale, coords,
colors, figures, shadingFill ? null : background);
if (!shadingFill) {
ctx.setTransform.apply(ctx, owner.baseTransform);
if (matrix) {
ctx.transform.apply(ctx, matrix);
}
}
ctx.translate(temporaryPatternCanvas.offsetX,
temporaryPatternCanvas.offsetY);
ctx.scale(temporaryPatternCanvas.scaleX,
temporaryPatternCanvas.scaleY);
return ctx.createPattern(temporaryPatternCanvas.canvas, 'no-repeat');
}
};
}
};
ShadingIRs.Dummy = {
fromIR: function Dummy_fromIR() {
return {
type: 'Pattern',
getPattern: function Dummy_fromIR_getPattern() {
return 'hotpink';
}
};
}
};
function getShadingPatternFromIR(raw) {
var shadingIR = ShadingIRs[raw[0]];
if (!shadingIR) {
error('Unknown IR type: ' + raw[0]);
}
return shadingIR.fromIR(raw);
}
var TilingPattern = (function TilingPatternClosure() {
var PaintType = {
COLORED: 1,
UNCOLORED: 2
};
var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough
function TilingPattern(IR, color, ctx, objs, commonObjs, baseTransform) {
this.operatorList = IR[2];
this.matrix = IR[3] || [1, 0, 0, 1, 0, 0];
this.bbox = IR[4];
this.xstep = IR[5];
this.ystep = IR[6];
this.paintType = IR[7];
this.tilingType = IR[8];
this.color = color;
this.objs = objs;
this.commonObjs = commonObjs;
this.baseTransform = baseTransform;
this.type = 'Pattern';
this.ctx = ctx;
}
TilingPattern.prototype = {
createPatternCanvas: function TilinPattern_createPatternCanvas(owner) {
var operatorList = this.operatorList;
var bbox = this.bbox;
var xstep = this.xstep;
var ystep = this.ystep;
var paintType = this.paintType;
var tilingType = this.tilingType;
var color = this.color;
var objs = this.objs;
var commonObjs = this.commonObjs;
info('TilingType: ' + tilingType);
var x0 = bbox[0], y0 = bbox[1], x1 = bbox[2], y1 = bbox[3];
var topLeft = [x0, y0];
// we want the canvas to be as large as the step size
var botRight = [x0 + xstep, y0 + ystep];
var width = botRight[0] - topLeft[0];
var height = botRight[1] - topLeft[1];
// Obtain scale from matrix and current transformation matrix.
var matrixScale = Util.singularValueDecompose2dScale(this.matrix);
var curMatrixScale = Util.singularValueDecompose2dScale(
this.baseTransform);
var combinedScale = [matrixScale[0] * curMatrixScale[0],
matrixScale[1] * curMatrixScale[1]];
// MAX_PATTERN_SIZE is used to avoid OOM situation.
// Use width and height values that are as close as possible to the end
// result when the pattern is used. Too low value makes the pattern look
// blurry. Too large value makes it look too crispy.
width = Math.min(Math.ceil(Math.abs(width * combinedScale[0])),
MAX_PATTERN_SIZE);
height = Math.min(Math.ceil(Math.abs(height * combinedScale[1])),
MAX_PATTERN_SIZE);
var tmpCanvas = CachedCanvases.getCanvas('pattern', width, height, true);
var tmpCtx = tmpCanvas.context;
var graphics = new CanvasGraphics(tmpCtx, commonObjs, objs);
graphics.groupLevel = owner.groupLevel;
this.setFillAndStrokeStyleToContext(tmpCtx, paintType, color);
this.setScale(width, height, xstep, ystep);
this.transformToScale(graphics);
// transform coordinates to pattern space
var tmpTranslate = [1, 0, 0, 1, -topLeft[0], -topLeft[1]];
graphics.transform.apply(graphics, tmpTranslate);
this.clipBbox(graphics, bbox, x0, y0, x1, y1);
graphics.executeOperatorList(operatorList);
return tmpCanvas.canvas;
},
setScale: function TilingPattern_setScale(width, height, xstep, ystep) {
this.scale = [width / xstep, height / ystep];
},
transformToScale: function TilingPattern_transformToScale(graphics) {
var scale = this.scale;
var tmpScale = [scale[0], 0, 0, scale[1], 0, 0];
graphics.transform.apply(graphics, tmpScale);
},
scaleToContext: function TilingPattern_scaleToContext() {
var scale = this.scale;
this.ctx.scale(1 / scale[0], 1 / scale[1]);
},
clipBbox: function clipBbox(graphics, bbox, x0, y0, x1, y1) {
if (bbox && isArray(bbox) && bbox.length === 4) {
var bboxWidth = x1 - x0;
var bboxHeight = y1 - y0;
graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight);
graphics.clip();
graphics.endPath();
}
},
setFillAndStrokeStyleToContext:
function setFillAndStrokeStyleToContext(context, paintType, color) {
switch (paintType) {
case PaintType.COLORED:
var ctx = this.ctx;
context.fillStyle = ctx.fillStyle;
context.strokeStyle = ctx.strokeStyle;
break;
case PaintType.UNCOLORED:
var cssColor = Util.makeCssRgb(color[0], color[1], color[2]);
context.fillStyle = cssColor;
context.strokeStyle = cssColor;
break;
default:
error('Unsupported paint type: ' + paintType);
}
},
getPattern: function TilingPattern_getPattern(ctx, owner) {
var temporaryPatternCanvas = this.createPatternCanvas(owner);
ctx = this.ctx;
ctx.setTransform.apply(ctx, this.baseTransform);
ctx.transform.apply(ctx, this.matrix);
this.scaleToContext();
return ctx.createPattern(temporaryPatternCanvas, 'repeat');
}
};
return TilingPattern;
})();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,437 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2014 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals PDFJS, shadow */
/* jshint -W043 */
'use strict';
var WebGLUtils = (function WebGLUtilsClosure() {
function loadShader(gl, code, shaderType) {
var shader = gl.createShader(shaderType);
gl.shaderSource(shader, code);
gl.compileShader(shader);
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var errorMsg = gl.getShaderInfoLog(shader);
throw new Error('Error during shader compilation: ' + errorMsg);
}
return shader;
}
function createVertexShader(gl, code) {
return loadShader(gl, code, gl.VERTEX_SHADER);
}
function createFragmentShader(gl, code) {
return loadShader(gl, code, gl.FRAGMENT_SHADER);
}
function createProgram(gl, shaders) {
var program = gl.createProgram();
for (var i = 0, ii = shaders.length; i < ii; ++i) {
gl.attachShader(program, shaders[i]);
}
gl.linkProgram(program);
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
var errorMsg = gl.getProgramInfoLog(program);
throw new Error('Error during program linking: ' + errorMsg);
}
return program;
}
function createTexture(gl, image, textureId) {
gl.activeTexture(textureId);
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set the parameters so we can render any size image.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// Upload the image into the texture.
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
return texture;
}
var currentGL, currentCanvas;
function generateGL() {
if (currentGL) {
return;
}
currentCanvas = document.createElement('canvas');
currentGL = currentCanvas.getContext('webgl',
{ premultipliedalpha: false });
}
var smaskVertexShaderCode = '\
attribute vec2 a_position; \
attribute vec2 a_texCoord; \
\
uniform vec2 u_resolution; \
\
varying vec2 v_texCoord; \
\
void main() { \
vec2 clipSpace = (a_position / u_resolution) * 2.0 - 1.0; \
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); \
\
v_texCoord = a_texCoord; \
} ';
var smaskFragmentShaderCode = '\
precision mediump float; \
\
uniform vec4 u_backdrop; \
uniform int u_subtype; \
uniform sampler2D u_image; \
uniform sampler2D u_mask; \
\
varying vec2 v_texCoord; \
\
void main() { \
vec4 imageColor = texture2D(u_image, v_texCoord); \
vec4 maskColor = texture2D(u_mask, v_texCoord); \
if (u_backdrop.a > 0.0) { \
maskColor.rgb = maskColor.rgb * maskColor.a + \
u_backdrop.rgb * (1.0 - maskColor.a); \
} \
float lum; \
if (u_subtype == 0) { \
lum = maskColor.a; \
} else { \
lum = maskColor.r * 0.3 + maskColor.g * 0.59 + \
maskColor.b * 0.11; \
} \
imageColor.a *= lum; \
imageColor.rgb *= imageColor.a; \
gl_FragColor = imageColor; \
} ';
var smaskCache = null;
function initSmaskGL() {
var canvas, gl;
generateGL();
canvas = currentCanvas;
currentCanvas = null;
gl = currentGL;
currentGL = null;
// setup a GLSL program
var vertexShader = createVertexShader(gl, smaskVertexShaderCode);
var fragmentShader = createFragmentShader(gl, smaskFragmentShaderCode);
var program = createProgram(gl, [vertexShader, fragmentShader]);
gl.useProgram(program);
var cache = {};
cache.gl = gl;
cache.canvas = canvas;
cache.resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
cache.positionLocation = gl.getAttribLocation(program, 'a_position');
cache.backdropLocation = gl.getUniformLocation(program, 'u_backdrop');
cache.subtypeLocation = gl.getUniformLocation(program, 'u_subtype');
var texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
var texLayerLocation = gl.getUniformLocation(program, 'u_image');
var texMaskLocation = gl.getUniformLocation(program, 'u_mask');
// provide texture coordinates for the rectangle.
var texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
1.0, 0.0,
1.0, 1.0]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(texCoordLocation);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.uniform1i(texLayerLocation, 0);
gl.uniform1i(texMaskLocation, 1);
smaskCache = cache;
}
function composeSMask(layer, mask, properties) {
var width = layer.width, height = layer.height;
if (!smaskCache) {
initSmaskGL();
}
var cache = smaskCache,canvas = cache.canvas, gl = cache.gl;
canvas.width = width;
canvas.height = height;
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.uniform2f(cache.resolutionLocation, width, height);
if (properties.backdrop) {
gl.uniform4f(cache.resolutionLocation, properties.backdrop[0],
properties.backdrop[1], properties.backdrop[2], 1);
} else {
gl.uniform4f(cache.resolutionLocation, 0, 0, 0, 0);
}
gl.uniform1i(cache.subtypeLocation,
properties.subtype === 'Luminosity' ? 1 : 0);
// Create a textures
var texture = createTexture(gl, layer, gl.TEXTURE0);
var maskTexture = createTexture(gl, mask, gl.TEXTURE1);
// Create a buffer and put a single clipspace rectangle in
// it (2 triangles)
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0, 0,
width, 0,
0, height,
0, height,
width, 0,
width, height]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(cache.positionLocation);
gl.vertexAttribPointer(cache.positionLocation, 2, gl.FLOAT, false, 0, 0);
// draw
gl.clearColor(0, 0, 0, 0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.flush();
gl.deleteTexture(texture);
gl.deleteTexture(maskTexture);
gl.deleteBuffer(buffer);
return canvas;
}
var figuresVertexShaderCode = '\
attribute vec2 a_position; \
attribute vec3 a_color; \
\
uniform vec2 u_resolution; \
uniform vec2 u_scale; \
uniform vec2 u_offset; \
\
varying vec4 v_color; \
\
void main() { \
vec2 position = (a_position + u_offset) * u_scale; \
vec2 clipSpace = (position / u_resolution) * 2.0 - 1.0; \
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); \
\
v_color = vec4(a_color / 255.0, 1.0); \
} ';
var figuresFragmentShaderCode = '\
precision mediump float; \
\
varying vec4 v_color; \
\
void main() { \
gl_FragColor = v_color; \
} ';
var figuresCache = null;
function initFiguresGL() {
var canvas, gl;
generateGL();
canvas = currentCanvas;
currentCanvas = null;
gl = currentGL;
currentGL = null;
// setup a GLSL program
var vertexShader = createVertexShader(gl, figuresVertexShaderCode);
var fragmentShader = createFragmentShader(gl, figuresFragmentShaderCode);
var program = createProgram(gl, [vertexShader, fragmentShader]);
gl.useProgram(program);
var cache = {};
cache.gl = gl;
cache.canvas = canvas;
cache.resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
cache.scaleLocation = gl.getUniformLocation(program, 'u_scale');
cache.offsetLocation = gl.getUniformLocation(program, 'u_offset');
cache.positionLocation = gl.getAttribLocation(program, 'a_position');
cache.colorLocation = gl.getAttribLocation(program, 'a_color');
figuresCache = cache;
}
function drawFigures(width, height, backgroundColor, figures, context) {
if (!figuresCache) {
initFiguresGL();
}
var cache = figuresCache, canvas = cache.canvas, gl = cache.gl;
canvas.width = width;
canvas.height = height;
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.uniform2f(cache.resolutionLocation, width, height);
// count triangle points
var count = 0;
var i, ii, rows;
for (i = 0, ii = figures.length; i < ii; i++) {
switch (figures[i].type) {
case 'lattice':
rows = (figures[i].coords.length / figures[i].verticesPerRow) | 0;
count += (rows - 1) * (figures[i].verticesPerRow - 1) * 6;
break;
case 'triangles':
count += figures[i].coords.length;
break;
}
}
// transfer data
var coords = new Float32Array(count * 2);
var colors = new Uint8Array(count * 3);
var coordsMap = context.coords, colorsMap = context.colors;
var pIndex = 0, cIndex = 0;
for (i = 0, ii = figures.length; i < ii; i++) {
var figure = figures[i], ps = figure.coords, cs = figure.colors;
switch (figure.type) {
case 'lattice':
var cols = figure.verticesPerRow;
rows = (ps.length / cols) | 0;
for (var row = 1; row < rows; row++) {
var offset = row * cols + 1;
for (var col = 1; col < cols; col++, offset++) {
coords[pIndex] = coordsMap[ps[offset - cols - 1]];
coords[pIndex + 1] = coordsMap[ps[offset - cols - 1] + 1];
coords[pIndex + 2] = coordsMap[ps[offset - cols]];
coords[pIndex + 3] = coordsMap[ps[offset - cols] + 1];
coords[pIndex + 4] = coordsMap[ps[offset - 1]];
coords[pIndex + 5] = coordsMap[ps[offset - 1] + 1];
colors[cIndex] = colorsMap[cs[offset - cols - 1]];
colors[cIndex + 1] = colorsMap[cs[offset - cols - 1] + 1];
colors[cIndex + 2] = colorsMap[cs[offset - cols - 1] + 2];
colors[cIndex + 3] = colorsMap[cs[offset - cols]];
colors[cIndex + 4] = colorsMap[cs[offset - cols] + 1];
colors[cIndex + 5] = colorsMap[cs[offset - cols] + 2];
colors[cIndex + 6] = colorsMap[cs[offset - 1]];
colors[cIndex + 7] = colorsMap[cs[offset - 1] + 1];
colors[cIndex + 8] = colorsMap[cs[offset - 1] + 2];
coords[pIndex + 6] = coords[pIndex + 2];
coords[pIndex + 7] = coords[pIndex + 3];
coords[pIndex + 8] = coords[pIndex + 4];
coords[pIndex + 9] = coords[pIndex + 5];
coords[pIndex + 10] = coordsMap[ps[offset]];
coords[pIndex + 11] = coordsMap[ps[offset] + 1];
colors[cIndex + 9] = colors[cIndex + 3];
colors[cIndex + 10] = colors[cIndex + 4];
colors[cIndex + 11] = colors[cIndex + 5];
colors[cIndex + 12] = colors[cIndex + 6];
colors[cIndex + 13] = colors[cIndex + 7];
colors[cIndex + 14] = colors[cIndex + 8];
colors[cIndex + 15] = colorsMap[cs[offset]];
colors[cIndex + 16] = colorsMap[cs[offset] + 1];
colors[cIndex + 17] = colorsMap[cs[offset] + 2];
pIndex += 12;
cIndex += 18;
}
}
break;
case 'triangles':
for (var j = 0, jj = ps.length; j < jj; j++) {
coords[pIndex] = coordsMap[ps[j]];
coords[pIndex + 1] = coordsMap[ps[j] + 1];
colors[cIndex] = colorsMap[cs[i]];
colors[cIndex + 1] = colorsMap[cs[j] + 1];
colors[cIndex + 2] = colorsMap[cs[j] + 2];
pIndex += 2;
cIndex += 3;
}
break;
}
}
// draw
if (backgroundColor) {
gl.clearColor(backgroundColor[0] / 255, backgroundColor[1] / 255,
backgroundColor[2] / 255, 1.0);
} else {
gl.clearColor(0, 0, 0, 0);
}
gl.clear(gl.COLOR_BUFFER_BIT);
var coordsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, coordsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, coords, gl.STATIC_DRAW);
gl.enableVertexAttribArray(cache.positionLocation);
gl.vertexAttribPointer(cache.positionLocation, 2, gl.FLOAT, false, 0, 0);
var colorsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
gl.enableVertexAttribArray(cache.colorLocation);
gl.vertexAttribPointer(cache.colorLocation, 3, gl.UNSIGNED_BYTE, false,
0, 0);
gl.uniform2f(cache.scaleLocation, context.scaleX, context.scaleY);
gl.uniform2f(cache.offsetLocation, context.offsetX, context.offsetY);
gl.drawArrays(gl.TRIANGLES, 0, count);
gl.flush();
gl.deleteBuffer(coordsBuffer);
gl.deleteBuffer(colorsBuffer);
return canvas;
}
function cleanup() {
if (smaskCache && smaskCache.canvas) {
smaskCache.canvas.width = 0;
smaskCache.canvas.height = 0;
}
if (figuresCache && figuresCache.canvas) {
figuresCache.canvas.width = 0;
figuresCache.canvas.height = 0;
}
smaskCache = null;
figuresCache = null;
}
return {
get isEnabled() {
if (PDFJS.disableWebGL) {
return false;
}
var enabled = false;
try {
generateGL();
enabled = !!currentGL;
} catch (e) { }
return shadow(this, 'isEnabled', enabled);
},
composeSMask: composeSMask,
drawFigures: drawFigures,
clear: cleanup
};
})();
@@ -0,0 +1,34 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
NOTE: This file is created as a helper to assist with JSDoc html files.
It is not for use in the executable code.
*/
/**
* PDFJS scope object that contains all functions, objects and variables related
* to the PDF.js.
* @constructor
*/
function PDFJS() {
// Mock class constructor. See src/display/api.js.
}
/**
* Represents the eventual result of an asynchronous operation.
* @external Promise
* @see {@link http://promisesaplus.com/ Promise/A+}
*/
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64">
<path
d="M 4.8364028,0.4891005 32.096378,4.5726641 59.163597,0.4891005 54.680408,57.805097 32.096378,63.510899 8.3116209,57.805097 z"
style="fill:#e5e7e8;fill-opacity:1;fill-rule:nonzero;stroke:#cccccc" />
<path
d="M 32.096378,10.745857 53.925414,6.8301117 51.016574,53.81906 32.096378,58.517955 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" />
<rect
width="34.027256"
height="19.136194"
x="3.7557135"
y="22.431904"
style="fill:#ff2600;fill-opacity:1;fill-rule:nonzero;stroke:none" />
<rect
width="23.480518"
height="19.136194"
x="36.763767"
y="22.431904"
style="fill:#ff501a;fill-opacity:1;fill-rule:nonzero;stroke:none" />
<g transform="matrix(0.42778543,0,0,0.42778543,58.617711,9.6737064)">
<path
d="m -120.53125,34.59375 0,35.1875 6.53125,0 0,-5.9375 0,-5.96875 8.875,0 4.15625,-3.71875 0,-7.71875 0,-7.71875 -4.21875,-4.125 -15.34375,0 z m 6.53125,6.8125 6.21875,0 0,10.21875 -6.21875,0 0,-10.21875 z"
style="fill:#ffffff;fill-opacity:1" />
<path
d="m -98.125,34.59375 0,35.1875 16.125,0 3.75,-3.625 0,-27.96875 -3.96875,-3.59375 -15.90625,0 z m 6.8125,6.8125 6.8125,0 0,21.5625 -6.8125,0 0,-21.5625 z" id="path3056"
style="fill:#ffffff;fill-opacity:1" />
<path
d="m -74.856072,34.602929 c 5.485697,0 10.971394,0 16.457091,0 0,2.269943 0,4.539887 0,6.80983 -3.404915,0 -6.809831,0 -10.214746,0 0,2.472069 0,4.944138 0,7.416206 2.93201,0.110496 5.864021,-0.110494 8.796031,0 l 0,3.366025 0,3.375914 c -2.93201,0.110495 -5.864021,-0.110495 -8.796031,0 l 0,7.146446 0,7.069702 c -2.080782,0 -4.161563,0 -6.242345,0 0,-11.728041 0,-23.456082 0,-35.184123 z"
style="fill:#ffffff;fill-opacity:1" />
<path
d="m -42.8813,67.942723 -2.181762,-1.844329 c 0,-1.986871 0,-3.973742 0,-5.960613 2.175363,6.7e-4 4.350725,0.0013 6.526088,0.002 0,0.94581 0,1.891619 0,2.837429 1.891619,0 3.783239,0 5.674858,0 0,-9.458098 0,-18.916195 0,-28.374293 2.175363,-1.26e-4 4.350725,-2.52e-4 6.526088,-3.78e-4 0,10.498614 0,20.997229 0,31.495843 -1.454508,1.229554 -2.909019,2.459106 -4.363529,3.688658 -3.333325,0 -6.666651,0 -9.999976,0 -0.727237,-0.614768 -1.454563,-1.229595 -2.181767,-1.844308 z"
style="fill:#ffffff;fill-opacity:1" />
<path
d="m -21.316836,67.942723 -2.109122,-1.844329 0,-5.978379 c 2.061011,0 4.392437,0.01978 6.453448,0.01978 0,0.94581 0,1.891619 0,2.837429 2.269943,0 4.539887,0 6.80983,0 0,-2.459105 0,-4.918211 0,-7.377316 -3.196954,0 -6.393909,0 -9.590863,0 l -3.745055,-3.688659 0,-6.80983 0,-6.80983 3.745055,-3.688658 c 3.863156,0 7.726313,0 11.5894692,0 l 4.2511106,3.688658 c -0.00204,1.797039 -0.00472,3.594077 -0.00737,5.391115 -2.0807816,0 -4.1615632,0 -6.2423448,0 0,-0.756648 0,-1.513295 0,-2.269943 -2.269943,0 -4.539887,0 -6.80983,0 0,2.459105 0,4.918211 0,7.377316 2.990126,0 5.980253,0 8.9703792,0 1.3605964,1.204585 4.0817956,3.613749 4.0817956,3.613749 0,4.555246 0,9.110492 0,13.665738 l -2.0892845,1.858745 -2.0892901,1.858745 -5.5180792,0 -5.51808,0 z"
style="fill:#ffffff;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,52 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*jshint globalstrict: false */
/* globals PDFJS */
// Initializing PDFJS global object (if still undefined)
if (typeof PDFJS === 'undefined') {
(typeof window !== 'undefined' ? window : this).PDFJS = {};
}
//#if BUNDLE_VERSION
//#expand PDFJS.version = '__BUNDLE_VERSION__';
//#endif
//#if BUNDLE_BUILD
//#expand PDFJS.build = '__BUNDLE_BUILD__';
//#endif
(function pdfjsWrapper() {
// Use strict in our context only - users might not want it
'use strict';
//#expand __BUNDLE__
}).call((typeof window === 'undefined') ? this : window);
//#if !(MOZCENTRAL || FIREFOX)
if (!PDFJS.workerSrc && typeof document !== 'undefined') {
console.log("should not be used");
// workerSrc is not set -- using last script url to define default location
PDFJS.workerSrc = (function () {
'use strict';
var scriptTagContainer = document.body ||
document.getElementsByTagName('head')[0];
var pdfjsSrc = scriptTagContainer.lastChild.src;
return pdfjsSrc && pdfjsSrc.replace(/\.js$/i, '.worker.js');
})();
}
//#endif
@@ -0,0 +1,314 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var CFFEncodingMap = {
'0': '-reserved-',
'1': 'hstem',
'2': '-reserved-',
'3': 'vstem',
'4': 'vmoveto',
'5': 'rlineto',
'6': 'hlineto',
'7': 'vlineto',
'8': 'rrcurveto',
'9': '-reserved-',
'10': 'callsubr',
'11': 'return',
'12': {
'3': 'and',
'4': 'or',
'5': 'not',
'9': 'abs',
'10': 'add',
'11': 'div',
'12': 'sub',
'14': 'neg',
'15': 'eq',
'18': 'drop',
'20': 'put',
'21': 'get',
'22': 'ifelse',
'23': 'random',
'24': 'mul',
'26': 'sqrt',
'27': 'dup',
'28': 'exch',
'29': 'index',
'30': 'roll',
'34': 'hflex',
'35': 'flex',
'36': 'hflex1',
'37': 'flex1'
},
'13': '-reserved-',
'14': 'endchar',
'15': '-reserved-',
'16': '-reserved-',
'17': '-reserved-',
'18': 'hstemhm',
'19': 'hintmask',
'20': 'cntrmask',
'21': 'rmoveto',
'22': 'hmoveto',
'23': 'vstemhm',
'24': 'rcurveline',
'25': 'rlivecurve',
'26': 'vvcurveto',
'27': 'hhcurveto',
'29': 'callgsubr',
'30': 'vhcurveto',
'31': 'hvcurveto'
};
var CFFDictDataMap = {
'0': {
name: 'version',
operand: 'SID'
},
'1': {
name: 'Notice',
operand: 'SID'
},
'2': {
name: 'FullName',
operand: 'SID'
},
'3': {
name: 'FamilyName',
operand: 'SID'
},
'4': {
name: 'Weight',
operand: 'SID'
},
'5': {
name: 'FontBBox',
operand: [0, 0, 0, 0]
},
'6': {
name: 'BlueValues'
},
'7': {
name: 'OtherBlues'
},
'8': {
name: 'FamilyBlues'
},
'9': {
name: 'FamilyOtherBlues'
},
'10': {
name: 'StdHW'
},
'11': {
name: 'StdVW'
},
'12': {
'0': {
name: 'Copyright',
operand: 'SID'
},
'1': {
name: 'IsFixedPitch',
operand: false
},
'2': {
name: 'ItalicAngle',
operand: 0
},
'3': {
name: 'UnderlinePosition',
operand: -100
},
'4': {
name: 'UnderlineThickness',
operand: 50
},
'5': {
name: 'PaintType',
operand: 0
},
'6': {
name: 'CharstringType',
operand: 2
},
'7': {
name: 'FontMatrix',
operand: [0.001, 0, 0, 0.001, 0 , 0]
},
'8': {
name: 'StrokeWidth',
operand: 0
},
'9': {
name: 'BlueScale'
},
'10': {
name: 'BlueShift'
},
'11': {
name: 'BlueFuzz'
},
'12': {
name: 'StemSnapH'
},
'13': {
name: 'StemSnapV'
},
'14': {
name: 'ForceBold'
},
'17': {
name: 'LanguageGroup'
},
'18': {
name: 'ExpansionFactor'
},
'19': {
name: 'initialRandomSeed'
},
'20': {
name: 'SyntheticBase',
operand: null
},
'21': {
name: 'PostScript',
operand: 'SID'
},
'22': {
name: 'BaseFontName',
operand: 'SID'
},
'23': {
name: 'BaseFontBlend',
operand: 'delta'
}
},
'13': {
name: 'UniqueID',
operand: null
},
'14': {
name: 'XUID',
operand: []
},
'15': {
name: 'charset',
operand: 0
},
'16': {
name: 'Encoding',
operand: 0
},
'17': {
name: 'CharStrings',
operand: null
},
'18': {
name: 'Private',
operand: 'number number'
},
'19': {
name: 'Subrs'
},
'20': {
name: 'defaultWidthX'
},
'21': {
name: 'nominalWidthX'
}
};
var CFFDictPrivateDataMap = {
'6': {
name: 'BluesValues',
operand: 'delta'
},
'7': {
name: 'OtherBlues',
operand: 'delta'
},
'8': {
name: 'FamilyBlues',
operand: 'delta'
},
'9': {
name: 'FamilyOtherBlues',
operand: 'delta'
},
'10': {
name: 'StdHW',
operand: null
},
'11': {
name: 'StdVW',
operand: null
},
'12': {
'9': {
name: 'BlueScale',
operand: 0.039625
},
'10': {
name: 'BlueShift',
operand: 7
},
'11': {
name: 'BlueFuzz',
operand: 1
},
'12': {
name: 'StemSnapH',
operand: 'delta'
},
'13': {
name: 'StemSnapV',
operand: 'delta'
},
'14': {
name: 'ForceBold',
operand: 'boolean'
},
'17': {
name: 'LanguageGroup',
operand: 0
},
'18': {
name: 'ExpansionFactor',
operand: 0.06
},
'19': {
name: 'initialRandomSeed',
operand: 0
}
},
'19': {
name: 'Subrs',
operand: null
},
'20': {
name: 'defaultWidthX',
operand: 0
},
'21': {
name: 'nominalWidthX',
operand: 0
}
};
@@ -0,0 +1,434 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals CFFDictDataMap, CFFDictPrivateDataMap, CFFEncodingMap, CFFStrings,
Components, Dict, dump, error, isNum, netscape, Stream */
'use strict';
/*
* The Type2 reader code below is only used for debugging purpose since Type2
* is only a CharString format and is never used directly as a Font file.
*
* So the code here is useful for dumping the data content of a .cff file in
* order to investigate the similarity between a Type1 CharString and a Type2
* CharString or to understand the structure of the CFF format.
*/
/*
* Build a charset by assigning the glyph name and the human readable form
* of the glyph data.
*/
function readCharset(aStream, aCharstrings) {
var charset = {};
var format = aStream.getByte();
var count = aCharstrings.length - 1;
var i, sid;
if (format === 0) {
charset['.notdef'] = readCharstringEncoding(aCharstrings[0]);
for (i = 1; i < count + 1; i++) {
sid = aStream.getByte() << 8 | aStream.getByte();
charset[CFFStrings[sid]] = readCharstringEncoding(aCharstrings[i]);
}
} else if (format === 1) {
for (i = 1; i < count + 1; i++) {
var first = aStream.getByte();
first = (first << 8) | aStream.getByte();
var numLeft = aStream.getByte();
for (var j = 0; j <= numLeft; j++) {
sid = first++;
charset[CFFStrings[sid]] = readCharstringEncoding(aCharstrings[j]);
}
}
} else {
error('Invalid charset format');
}
return charset;
}
/*
* Take a Type2 binary charstring as input and transform it to a human
* readable representation as specified by the 'The Type 2 Charstring Format',
* chapter 3.1.
*/
function readCharstringEncoding(aString) {
if (!aString) {
return '';
}
var charstringTokens = [];
var count = aString.length;
for (var i = 0; i < count; ) {
var value = aString[i++] | 0;
var token = null;
if (value < 0) {
continue;
} else if (value <= 11) {
token = CFFEncodingMap[value];
} else if (value === 12) {
token = CFFEncodingMap[value][aString[i++]];
} else if (value <= 18) {
token = CFFEncodingMap[value];
} else if (value <= 20) {
++i; // var mask = aString[i++];
token = CFFEncodingMap[value];
} else if (value <= 27) {
token = CFFEncodingMap[value];
} else if (value === 28) {
token = aString[i++] << 8 | aString[i++];
} else if (value <= 31) {
token = CFFEncodingMap[value];
} else if (value < 247) {
token = parseInt(value, 10) - 139;
} else if (value < 251) {
token = (value - 247) * 256 + aString[i++] + 108;
} else if (value < 255) {
token = -(value - 251) * 256 - aString[i++] - 108;
} else { // value === 255
token = aString[i++] << 24 | aString[i++] << 16 |
aString[i++] << 8 | aString[i];
}
charstringTokens.push(token);
}
return charstringTokens;
}
/*
* Take a binary DICT Data as input and transform it into a human readable
* form as specified by 'The Compact Font Format Specification', chapter 5.
*/
function readFontDictData(aString, aMap) {
var fontDictDataTokens = [];
var count = aString.length;
for (var i = 0; i < count; i) {
var value = aString[i++] | 0;
var token = null;
if (value === 12) {
token = aMap[value][aString[i++]];
} else if (value === 28) {
token = aString[i++] << 8 | aString[i++];
} else if (value === 29) {
token = aString[i++] << 24 |
aString[i++] << 16 |
aString[i++] << 8 |
aString[i++];
} else if (value === 30) {
token = '';
var parsed = false;
while (!parsed) {
var octet = aString[i++];
var nibbles = [parseInt(octet / 16, 10), parseInt(octet % 16, 10)];
for (var j = 0; j < nibbles.length; j++) {
var nibble = nibbles[j];
switch (nibble) {
case 0xA:
token += '.';
break;
case 0xB:
token += 'E';
break;
case 0xC:
token += 'E-';
break;
case 0xD:
break;
case 0xE:
token += '-';
break;
case 0xF:
parsed = true;
break;
default:
token += nibble;
break;
}
}
}
token = parseFloat(token);
} else if (value <= 31) {
token = aMap[value];
} else if (value <= 246) {
token = parseInt(value, 10) - 139;
} else if (value <= 250) {
token = (value - 247) * 256 + aString[i++] + 108;
} else if (value <= 254) {
token = -(value - 251) * 256 - aString[i++] - 108;
} else if (value === 255) {
error('255 is not a valid DICT command');
}
fontDictDataTokens.push(token);
}
return fontDictDataTokens;
}
/*
* Take a stream as input and return an array of objects.
* In CFF an INDEX is a structure with the following format:
* {
* count: 2 bytes (Number of objects stored in INDEX),
* offsize: 1 byte (Offset array element size),
* offset: [count + 1] bytes (Offsets array),
* data: - (Objects data)
* }
*
* More explanation are given in the 'CFF Font Format Specification',
* chapter 5.
*/
function readFontIndexData(aStream, aIsByte) {
var count = aStream.getByte() << 8 | aStream.getByte();
var offsize = aStream.getByte();
function getNextOffset() {
switch (offsize) {
case 0:
return 0;
case 1:
return aStream.getByte();
case 2:
return aStream.getByte() << 8 | aStream.getByte();
case 3:
return aStream.getByte() << 16 | aStream.getByte() << 8 |
aStream.getByte();
case 4:
return aStream.getByte() << 24 | aStream.getByte() << 16 |
aStream.getByte() << 8 | aStream.getByte();
}
error(offsize + ' is not a valid offset size');
return null;
}
var offsets = [];
var i;
for (i = 0; i < count + 1; i++) {
offsets.push(getNextOffset());
}
dump('Found ' + count + ' objects at offsets :' +
offsets + ' (offsize: ' + offsize + ')');
// Now extract the objects
var relativeOffset = aStream.pos;
var objects = [];
for (i = 0; i < count; i++) {
var offset = offsets[i];
aStream.pos = relativeOffset + offset - 1;
var data = [];
var length = offsets[i + 1] - 1;
for (var j = offset - 1; j < length; j++) {
data.push(aIsByte ? aStream.getByte() : aStream.getChar());
}
objects.push(data);
}
return objects;
}
var Type2Parser = function type2Parser(aFilePath) {
var font = new Dict(null);
var xhr = new XMLHttpRequest();
xhr.open('GET', aFilePath, false);
xhr.responseType = 'arraybuffer';
xhr.expected = (document.URL.indexOf('file:') === 0) ? 0 : 200;
xhr.send(null);
this.data = new Stream(xhr.response);
// Turn on this flag for additional debugging logs
var debug = false;
function dump(aStr) {
if (debug) {
console.log(aStr);
}
}
function parseAsToken(aString, aMap) {
var decoded = readFontDictData(aString, aMap);
var stack = [];
var count = decoded.length;
for (var i = 0; i < count; i++) {
var token = decoded[i];
if (isNum(token)) {
stack.push(token);
} else {
switch (token.operand) {
case 'SID':
font.set(token.name, CFFStrings[stack.pop()]);
break;
case 'number number':
font.set(token.name, {
offset: stack.pop(),
size: stack.pop()
});
break;
case 'boolean':
font.set(token.name, stack.pop());
break;
case 'delta':
font.set(token.name, stack.pop());
break;
default:
if (token.operand && token.operand.length) {
var array = [];
for (var j = 0; j < token.operand.length; j++) {
array.push(stack.pop());
}
font.set(token.name, array);
} else {
font.set(token.name, stack.pop());
}
break;
}
}
}
}
this.parse = function type2ParserParse(aStream) {
font.set('major', aStream.getByte());
font.set('minor', aStream.getByte());
font.set('hdrSize', aStream.getByte());
font.set('offsize', aStream.getByte());
// Read the NAME Index
dump('Reading Index: Names');
font.set('Names', readFontIndexData(aStream));
dump('Names: ' + font.get('Names'));
// Read the Top Dict Index
dump('Reading Index: TopDict');
var topDict = readFontIndexData(aStream, true);
dump('TopDict: ' + topDict);
// Read the String Index
dump('Reading Index: Strings');
var strings = readFontIndexData(aStream);
dump('strings: ' + strings);
// Fill up the Strings dictionary with the new unique strings
var i;
for (i = 0; i < strings.length; i++) {
CFFStrings.push(strings[i].join(''));
}
// Parse the TopDict operator
var count = topDict.length;
for (i = 0; i < count; i++) {
parseAsToken(topDict[i], CFFDictDataMap);
}
// Read the Global Subr Index that comes just after the Strings Index
// (cf. "The Compact Font Format Specification" Chapter 16)
dump('Reading Global Subr Index');
var subrs = readFontIndexData(aStream, true);
dump(subrs);
// Reading Private Dict
var priv = font.get('Private');
dump('Reading Private Dict (offset: ' + priv.offset +
' size: ' + priv.size + ')');
aStream.pos = priv.offset;
var privateDict = [];
for (i = 0; i < priv.size; i++) {
privateDict.push(aStream.getByte());
}
dump('privateData:' + privateDict);
parseAsToken(privateDict, CFFDictPrivateDataMap);
for (var p in font.map) {
dump(p + '::' + font.get(p));
}
// Read CharStrings Index
var charStringsOffset = font.get('CharStrings');
dump('Read CharStrings Index (offset: ' + charStringsOffset + ')');
aStream.pos = charStringsOffset;
var charStrings = readFontIndexData(aStream, true);
// Read Charset
dump('Read Charset for ' + charStrings.length + ' glyphs');
var charsetEntry = font.get('charset');
if (charsetEntry === 0) {
error('Need to support CFFISOAdobeCharset');
} else if (charsetEntry === 1) {
error('Need to support CFFExpert');
} else if (charsetEntry === 2) {
error('Need to support CFFExpertSubsetCharset');
} else {
aStream.pos = charsetEntry;
readCharset(aStream, charStrings);
}
};
};
/*
* To try the Type2 decoder on a local file in the current directory:
*
* var cff = new Type2Parser("file.cff");
* cff.parse(this.data);
*
* To try the Type2 decoder on a custom built CFF array:
*
* var file = new Uint8Array(cffFileArray, 0, cffFileSize);
* var parser = new Type2Parser();
* parser.parse(new Stream(file));
*
*/
/*
* Write to a file to the disk (works only on Firefox in privilege mode)
* but this is useful for dumping a font file to the disk and check with
* fontforge or the ots program what's wrong with the file.
*
* writeToFile(fontData, "/tmp/pdf.js." + fontCount + ".cff");
*/
function writeToFile(aBytes, aFilePath) {
if (!('netscape' in window)) {
return;
}
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
var Cc = Components.classes,
Ci = Components.interfaces;
var file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsILocalFile);
file.initWithPath(aFilePath);
var stream = Cc['@mozilla.org/network/file-output-stream;1']
.createInstance(Ci.nsIFileOutputStream);
stream.init(file, 0x04 | 0x08 | 0x20, 0x180, 0);
var bos = Cc['@mozilla.org/binaryoutputstream;1']
.createInstance(Ci.nsIBinaryOutputStream);
bos.setOutputStream(stream);
bos.writeByteArray(aBytes, aBytes.length);
stream.close();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,81 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals PDFJS, Util */
'use strict';
// List of shared files to include;
var sharedFiles = [
'shared/util.js'
];
// List of other files to include;
var otherFiles = [
'core/network.js',
'core/chunked_stream.js',
'core/pdf_manager.js',
'core/core.js',
'core/obj.js',
'core/charsets.js',
'core/annotation.js',
'core/function.js',
'core/colorspace.js',
'core/crypto.js',
'core/pattern.js',
'core/evaluator.js',
'core/cmap.js',
'core/fonts.js',
'core/font_renderer.js',
'core/glyphlist.js',
'core/image.js',
'core/metrics.js',
'core/parser.js',
'core/ps_parser.js',
'core/stream.js',
'core/worker.js',
'core/arithmetic_decoder.js',
'core/jpg.js',
'core/jpx.js',
'core/jbig2.js',
'core/bidi.js',
'core/murmurhash3.js'
];
function loadInOrder(index, path, files) {
if (index >= files.length) {
PDFJS.fakeWorkerFilesLoadedCapability.resolve();
return;
}
PDFJS.Util.loadScript(path + files[index],
loadInOrder.bind(null, ++index, path, files));
}
// Load all the files.
if (typeof PDFJS === 'undefined' || !PDFJS.fakeWorkerFilesLoadedCapability) {
var files = sharedFiles.concat(otherFiles);
for (var i = 0; i < files.length; i++) {
importScripts(files[i]);
}
} else {
var src = PDFJS.workerSrc;
var path = src.substr(0, src.indexOf('worker_loader.js'));
// If Util is available, we assume that shared files are already loaded. Can
// happen that they are not if PDF.js is bundled inside a special namespace.
var skipShared = typeof Util !== 'undefined';
var files = skipShared ? otherFiles : sharedFiles.concat(otherFiles);
loadInOrder(0, path, files);
}
@@ -0,0 +1,7 @@
/* globals PDFJS */
'use strict';
// Simply resolve the promise, signaling all the assets are loaded.
// Used since the assets are all included in odoo bundle
PDFJS.fakeWorkerFilesLoadedCapability.resolve();
@@ -0,0 +1,207 @@
/**
Homemade helper for browsing PDF document from page to page.
This is hightly inspired from https://github.com/mozilla/pdf.js/blob/master/examples/learning/prevnext.html
This lib requires PDF JS. It simply uses PDFjs and its promises.
DOC : http://mozilla.github.io/pdf.js/api/draft/api.js.html
*/
// !!!!!!!!! use window.PDFJS and not PDFJS
var PDFSlidesViewer = (function(){
function PDFSlidesViewer(pdf_url, $canvas, disableWorker){
// pdf variables
this.pdf = null;
this.pdf_url = pdf_url || false;
this.pdf_scale = 1.5;
this.pdf_page_total = 0;
this.pdf_page_current = 1; // default is the first page
// promise business
this.pageRendering = false;
this.pageNumPending = null;
//canvas
this.canvas = $canvas;
this.canvas_context = $canvas.getContext('2d');
// PDF JS business
/**
* Disable the web worker and run all code on the main thread. This will happen
* automatically if the browser doesn't support workers or sending typed arrays
* to workers.
* @var {boolean}
*
* disableWorker should be 'true' if the document came from another origin than the
* page (typically the 'embed case').
* @see http://en.wikipedia.org/wiki/Cross-origin_resource_sharing.
* this is equivalent to the use_cors option in openerpframework.js
*/
window.PDFJS.disableWorker = disableWorker || false;
};
/**
* Load the PDF document
* @param (optional) url : the url of the document to load
*/
PDFSlidesViewer.prototype.loadDocument = function(url) {
var self = this;
var pdf_url = url || this.pdf_url;
return window.PDFJS.getDocument(pdf_url).then(function (file_content) {
self.pdf = file_content;
self.pdf_page_total = file_content.numPages;
return file_content;
});
};
/**
* Get page info from document, resize canvas accordingly, and render page.
* @param page_number : Page number.
*/
PDFSlidesViewer.prototype.renderPage = function(page_number) {
var self = this;
this.pageRendering = true;
return this.pdf.getPage(page_number).then(function(page) {
// Each PDF page has its own viewport which defines the size in pixels and initial rotation.
var viewport = page.getViewport(self.pdf_scale);
self.canvas.height = viewport.height;
self.canvas.width = viewport.width;
// Render PDF page into canvas context
var renderContext = {
canvasContext: self.canvas_context,
viewport: viewport
};
var renderTask = page.render(renderContext);
// Wait for rendering to finish
return renderTask.promise.then(function () {
self.pageRendering = false;
if (self.pageNumPending !== null) {
// New page rendering is pending
renderPage(self.pageNumPending);
self.pageNumPending = null;
}
self.pdf_page_current = page_number;
return page_number;
});
});
};
/**
* If another page rendering in progress, waits until the rendering is
* finised. Otherwise, executes rendering immediately.
*/
PDFSlidesViewer.prototype.queueRenderPage = function(num) {
if(this.pageRendering) {
this.pageNumPending = num; // the queue is only the last elem
return Promise.resolve(num);
} else {
return this.renderPage(num);
}
}
/**
* Displays previous page.
*/
PDFSlidesViewer.prototype.previousPage = function() {
if (this.pdf_page_current <= 1) {
return Promise.resolve(false);
}
this.pdf_page_current--;
return this.queueRenderPage(this.pdf_page_current);
};
/**
* Displays next page.
*/
PDFSlidesViewer.prototype.nextPage = function() {
if (this.pdf_page_current >= this.pdf_page_total) {
return Promise.resolve(false);
}
this.pdf_page_current++;
return this.queueRenderPage(this.pdf_page_current);
};
/**
* Displays the given page.
*/
PDFSlidesViewer.prototype.changePage = function(num){
if(1 <= num <= this.pdf_page_total){
this.pdf_page_current = num;
return this.queueRenderPage(num);
}
return Promise.resolve(false);
}
/**
* Displays first page.
*/
PDFSlidesViewer.prototype.firstPage = function(){
this.pdf_page_current = 1;
return this.queueRenderPage(1);
}
/**
* Displays last page.
*/
PDFSlidesViewer.prototype.lastPage = function(){
this.pdf_page_current = this.pdf_page_total;
return this.queueRenderPage(this.pdf_page_total);
}
PDFSlidesViewer.prototype.toggleFullScreen = function(){
var el = this.canvas;
var isFullscreenAvailable = document.fullscreenEnabled || document.mozFullScreenEnabled || document.webkitFullscreenEnabled || document.msFullscreenEnabled || false;
if(isFullscreenAvailable){ // Full screen supported
// get the actual element in FullScreen mode (Null if no element)
var fullscreenElement = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement;
if (fullscreenElement) { // Exit the full screen mode
if (document.exitFullscreen) {
// W3C standard
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
// Firefox 10+, Firefox for Android
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
// Chrome 20+, Safari 6+, Opera 15+, Chrome for Android, Opera Mobile 16+
document.webkitExitFullscreen();
} else if (document.webkitCancelFullScreen) {
// Chrome 15+, Safari 5.1+
document.webkitCancelFullScreen();
} else if (document.msExitFullscreen) {
// IE 11+
document.msExitFullscreen();
}
}else { // Request to put the 'el' element in FullScreen mode
if (el.requestFullscreen) {
// W3C standard
el.requestFullscreen();
} else if (el.mozRequestFullScreen) {
// Firefox 10+, Firefox for Android
el.mozRequestFullScreen();
} else if (el.msRequestFullscreen) {
// IE 11+
el.msRequestFullscreen();
} else if (el.webkitRequestFullscreen) {
if (navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) {
// Safari 6+
el.webkitRequestFullscreen();
} else {
// Chrome 20+, Opera 15+, Chrome for Android, Opera Mobile 16+
el.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
}
} else if (el.webkitRequestFullScreen) {
if (navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) {
// Safari 5.1+
el.webkitRequestFullScreen();
} else {
// Chrome 15+
el.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
}
}
}
}else{
// Full screen not supported by the browser
console.error("ERROR : full screen not supported by web browser");
}
}
return PDFSlidesViewer;
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

@@ -0,0 +1,184 @@
/*global $, openerp, _, PDFJS */
$(document).ready(function () {
"use strict";
var website = openerp.website;
var _t = openerp._t;
var widget_parent = $('body');
website.slide = {};
website.slide.page_widgets = {};
$("timeago.timeago").each(function (index, el) {
var datetime = $(el).attr('datetime'),
datetime_obj = openerp.str_to_datetime(datetime),
// if presentation 7 days, 24 hours, 60 min, 60 second, 1000 millis old(one week)
// then return fix formate string else timeago
display_str = "";
if (datetime_obj && new Date().getTime() - datetime_obj.getTime() > 7 * 24 * 60 * 60 * 1000) {
display_str = datetime_obj.toDateString();
} else {
display_str = $.timeago(datetime_obj);
}
$(el).text(display_str);
});
/*
* Like/Dislike Buttons Widget
*/
website.slide.LikeButton = openerp.Widget.extend({
setElement: function($el){
this._super.apply(this, arguments);
this.$el.on('click', this, _.bind(this.apply_action, this));
},
apply_action: function(ev){
var button = $(ev.currentTarget);
var slide_id = button.data('slide-id');
var user_id = button.data('user-id');
var is_public = button.data('public-user');
var href = button.data('href');
if(is_public){
this.popover_alert(button, _.str.sprintf(_t('Please <a href="/web?redirect=%s">login</a> to vote this slide'), (document.URL)));
}else{
var target = button.find('.fa');
if (localStorage['slide_vote_' + slide_id] !== user_id.toString()) {
openerp.jsonRpc(href, 'call', {slide_id: slide_id}).then(function (data) {
target.text(data);
localStorage['slide_vote_' + slide_id] = user_id;
});
} else {
this.popover_alert(button, _t('You have already voted for this slide'));
}
}
},
popover_alert: function($el, message){
$el.popover({
trigger: 'focus',
placement: 'bottom',
container: 'body',
html: true,
content: function(){
return message;
}
}).popover('toggle');
},
});
website.slide.page_widgets['likeButton'] = new website.slide.LikeButton(widget_parent).setElement($('.oe_slide_js_like'));
website.slide.page_widgets['dislikeButton'] = new website.slide.LikeButton(widget_parent).setElement($('.oe_slide_js_unlike'));
/*
* Embedded Code Widget
*/
website.slide.SlideSocialEmbed = openerp.Widget.extend({
events: {
'change input' : 'change_page',
},
init: function(parent, max_page){
this._super(parent);
this.max_page = max_page || false;
},
change_page: function(ev){
ev.preventDefault();
var input = this.$('input');
var page = parseInt(input.val());
if (this.max_page && !(page > 0 && page <= this.max_page)) {
page = 1;
}
this.update_embedded_code(page);
},
update_embedded_code: function(page){
var embed_input = this.$('.slide_embed_code');
var new_code = embed_input.val().replace(/(page=).*?([^\d]+)/, '$1' + page + '$2');
embed_input.val(new_code);
},
});
$('iframe').ready(function() {
// TODO : make it work. For now, once the iframe is loaded, the value of #page_count is
// still now set (the pdf is still loading)
var max_page = $('iframe').contents().find('#page_count').val();
var slide_social_embed = new website.slide.SlideSocialEmbed($(this), max_page).setElement($('.oe_slide_js_embed_code_widget'));
});
/*
* Send by email Widget
*/
website.slide.ShareMail = openerp.Widget.extend({
events: {
'click button' : 'send_mail',
},
send_mail: function(ev){
var self = this;
var input = this.$('input');
var slide_id = this.$('button').data('slide-id');
if(input.val() && input[0].checkValidity()){
this.$el.removeClass('has-error');
openerp.jsonRpc('/slides/slide/send_share_email', 'call', {
slide_id: slide_id,
email: input.val(),
}).then(function () {
self.$el.html($('<div class="alert alert-info" role="alert"><strong>Thank you!</strong> Mail has been sent.</div>'));
});
}else{
this.$el.addClass('has-error');
input.focus();
}
},
});
website.slide.page_widgets['share_mail'] = new website.slide.ShareMail(widget_parent).setElement($('.oe_slide_js_share_email'));
/*
* Social Sharing Statistics Widget
*/
if ($('div#statistic').length) {
var socialgatter = function (app_url, url, callback) {
$.ajax({
url: app_url + url,
dataType: 'jsonp',
success: callback
});
};
var current_url = window.location.origin + window.location.pathname;
socialgatter('https://www.linkedin.com/countserv/count/share?url=', current_url, function (data) {
$('#linkedin-badge').text(data.count || 0);
$('#total-share').text(parseInt($('#total-share').text()) + parseInt($('#linkedin-badge').text()));
});
socialgatter('https://cdn.api.twitter.com/1/urls/count.json?url=', current_url, function (data) {
$('#twitter-badge').text(data.count || 0);
$('#total-share').text(parseInt($('#total-share').text()) + parseInt($('#twitter-badge').text()));
});
socialgatter('https://graph.facebook.com/?id=', current_url, function (data) {
$('#facebook-badge').text(data.shares || 0);
$('#total-share').text(parseInt($('#total-share').text()) + parseInt($('#facebook-badge').text()));
});
$.ajax({
url: 'https://clients6.google.com/rpc',
type: "POST",
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify([{
"method": "pos.plusones.get",
"id": "p",
"params": {
"nolog": true,
"id": current_url,
"source": "widget",
"userId": "@viewer",
"groupId": "@self"
},
// TDE NOTE: should there be a key here ?
"jsonrpc": "2.0",
"apiVersion": "v1"
}]),
success: function (data) {
$('#google-badge').text(data[0].result.metadata.globalCounts.count || 0);
$('#total-share').text(parseInt($('#total-share').text()) + parseInt($('#google-badge').text()));
},
});
}
});
@@ -0,0 +1,197 @@
/*global $, _, PDFJS, PDFSlidesViewer, Bootstrap */
/**
This file is a minimal version of the PDFViewer widget.
It is NOT use in the website_slides module, but it is
called when embedding a slide/video/document.
This code can depend on pdf.js, JQuery and Bootstrap
(see website_slides.slide_embed_assets bundle, in website_slides_embed.xml)
**/
$(document).ready(function () {
if($('#PDFViewer') && $('#PDFViewerCanvas')){ // check if presentation only
// define embedded viewer (minimal object of the website.slide.PDFViewer widget)
var EmbeddedViewer = function($viewer){
var self = this;
this.viewer = $viewer;
this.slide_url = $viewer.find('#PDFSlideViewer').data('slideurl');
this.slide_id = $viewer.find('#PDFSlideViewer').data('slideid');
this.defaultpage = parseInt($viewer.find('#PDFSlideViewer').data('defaultpage'));
this.canvas = $viewer.find('canvas')[0];
this.pdf_viewer = new PDFSlidesViewer(this.slide_url, this.canvas, true);
this.pdf_viewer.loadDocument().then(function(file_content){
self.on_loaded_file();
});
}
EmbeddedViewer.prototype.__proto__ = {
// jquery inside the object (like openerp.Widget)
$: function(selector){
return this.viewer.find($(selector));
},
// post process action (called in '.then()')
on_loaded_file: function(){
this.$('canvas').show();
this.$('#page_count').text(this.pdf_viewer.pdf_page_total);
this.$('#PDFViewerLoader').hide();
// init first page to display
var initpage = this.defaultpage;
var pageNum = (initpage > 0 && initpage <= this.pdf_viewer.pdf_page_total)? initpage : 1;
this.render_page(pageNum);
},
on_rendered_page: function(page_number){
if(page_number){
this.$('#page_number').val(page_number);
}
},
// page switching
render_page: function (page_number) {
this.pdf_viewer.renderPage(page_number).then(this.on_rendered_page);
},
change_page: function (ev) {
var page_asked = parseInt(this.$('#page_number').val(), 10);
if(1 <= page_asked && page_asked <= this.pdf_viewer.pdf_page_total){
this.pdf_viewer.changePage(page_asked).then(this.on_rendered_page);
}else{
// if page number out of range, reset the page_counter to the actual page
this.$('#page_number').val(this.pdf_viewer.pdf_page_current);
}
},
next: function (ev) {
var self = this;
this.pdf_viewer.nextPage().then(function(page_num){
if(page_num){
self.on_rendered_page(page_num);
}else{
if(self.pdf_viewer.pdf){ // avoid display suggestion when pdf is not loaded yet
self.display_suggested_slides();
}
}
});
},
previous: function (ev) {
var self = this;
this.pdf_viewer.previousPage().then(function(page_num){
if(page_num){
self.on_rendered_page(page_num);
}
self.$("#slide_suggest").hide();
});
},
first: function (ev) {
var self = this;
this.pdf_viewer.firstPage().then(function(page_num){
self.on_rendered_page(page_num);
self.$("#slide_suggest").hide();
});
},
last: function (ev) {
var self = this;
this.pdf_viewer.lastPage().then(function(page_num){
self.on_rendered_page(page_num);
self.$("#slide_suggest").hide();
});
},
// full screen mode
fullscreen: function (ev) {
this.pdf_viewer.toggleFullScreen();
},
// display suggestion displayed after last slide
display_suggested_slides: function () {
this.$("#slide_suggest").show();
},
};
// embedded pdf viewer
embedded_viewer = new EmbeddedViewer($('#PDFViewer'));
// bind the actions
$('#previous').on('click', function(){
embedded_viewer.previous();
});
$('#next').on('click', function(){
embedded_viewer.next();
});
$('#first').on('click', function(){
embedded_viewer.first();
});
$('#last').on('click', function(){
embedded_viewer.last();
});
$('#page_number').on('change', function(){
embedded_viewer.change_page();
});
$('#fullscreen').on('click',function(){
embedded_viewer.fullscreen();
});
// switching slide with keyboard
$(document).keydown(function (ev) {
if (ev.keyCode === 37) {
embedded_viewer.previous();
}
if (ev.keyCode === 39) {
embedded_viewer.next();
}
});
// display the option panels
$('.oe_slide_js_embed_option_link').on('click', function (ev) {
ev.preventDefault();
var toggleDiv = $(this).data('slide-option-id');
$('.oe_slide_embed_option').not(toggleDiv).each(function() {
$(this).hide();
});
$(toggleDiv).slideToggle();
});
// animation for the suggested slides
$('.oe_slides_suggestion_media').hover(
function () {
$(this).find('.oe_slides_suggestion_caption').stop().slideDown(250);
},
function () {
$(this).find('.oe_slides_suggestion_caption').stop().slideUp(250);
}
);
// embed widget page selector
$('.oe_slide_js_embed_code_widget input').on('change', function (e) {
var page = parseInt($(this).val());
if (!(page > 0 && page <= embedded_viewer.pdf_viewer.pdf_page_total)) {
page = 1;
}
var actual_code = embedded_viewer.$('.slide_embed_code').val();
var new_code = actual_code.replace(/(page=).*?([^\d]+)/, '$1' + page + '$2');
embedded_viewer.$('.slide_embed_code').val(new_code);
});
// To avoid create a dependancy to openerpframework.js, we use JQuery AJAX to post data instead of openerp.jsonRpc
$('.oe_slide_js_share_email button').on('click', function () {
var widget = $('.oe_slide_js_share_email');
var input = widget.find('input');
var slide_id = widget.find('button').data('slide-id');
if(input.val() && input[0].checkValidity()){
widget.removeClass('has-error');
$.ajax({
type: "POST",
dataType: 'json',
url: '/slides/slide/send_share_email',
contentType: "application/json; charset=utf-8",
data: JSON.stringify({'jsonrpc': "2.0", 'method': "call", "params": {'slide_id': slide_id, 'email': input.val()}}),
success: function (data) {
widget.html($('<div class="alert alert-info" role="alert"><strong>Thank you!</strong> Mail has been sent.</div>'));
},
error: function(data){
console.log("ERROR ", data);
}
});
}else{
widget.addClass('has-error');
input.focus();
}
});
}
});
@@ -0,0 +1,357 @@
/*global $, openerp, _, PDFJS */
$(document).ready(function () {
"use strict";
var website = openerp.website;
var _t = openerp._t;
website.slide.template = website.add_template_file('/website_slides/static/src/xml/website_slides.xml');
website.slide.Dialog = openerp.Widget.extend({
template: 'website.slide.upload',
events: {
'hidden.bs.modal': 'destroy',
'click button.save': 'save',
'click button[data-dismiss="modal"]': 'cancel',
'change input#upload': 'slide_upload',
'change input#url': 'slide_url',
'click .list-group-item': function (ev) {
this.$('.list-group-item').removeClass('active');
$(ev.target).closest('li').addClass('active');
}
},
init: function (el, channel_id) {
this._super(el, channel_id);
this.channel_id = parseInt(channel_id, 10);
this.file = {};
this.index_content = "";
},
start: function () {
this.$el.modal({
backdrop: 'static'
});
this.set_category_id();
this.set_tag_ids();
},
slide_url: function (ev) {
var self = this,
value = {
'url': $(ev.target).val(),
'channel_id': self.channel_id
};
this.$('.alert-warning').remove();
this.is_valid_url = false;
this.$('.save').button('loading');
openerp.jsonRpc('/slides/dialog_preview/', 'call', value).then(function (data) {
self.$('.save').button('reset');
if (data.error) {
self.display_alert(data.error);
} else {
self.$("#slide-image").attr("src", data.url_src);
self.$('#name').val(data.title);
self.$('#description').val(data.description);
self.is_valid_url = true;
}
});
},
check_unique_slide: function (file_name) {
var self = this;
return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'slide.slide',
method: 'search_count',
args: [[['channel_id', '=', self.channel_id], ['name', '=', file_name]]],
kwargs: {}
});
},
slide_upload: function (ev) {
var self = this,
file = ev.target.files[0],
is_image = /^image\/.*/.test(file.type),
loaded = false;
this.file.name = file.name;
this.file.type = file.type;
if (!(is_image || this.file.type === 'application/pdf')) {
this.display_alert(_t("Invalid file type. Please select pdf or image file"));
this.reset_file();
return;
}
if (file.size / 1024 / 1024 > 15) {
this.display_alert(_t("File is too big. Please select file upto 15MB"));
this.reset_file();
return;
}
this.$('.alert-warning').remove();
var BinaryReader = new FileReader();
// file read as DataURL
BinaryReader.readAsDataURL(file);
BinaryReader.onloadend = function (upload) {
var buffer = upload.target.result;
if (is_image) {
self.$("#slide-image").attr("src", buffer);
}
buffer = buffer.split(',')[1];
self.file.data = buffer;
};
if (file.type === 'application/pdf') {
var ArrayReader = new FileReader();
this.$('.save').button('loading');
// file read as ArrayBuffer for PDFJS get_Document API
ArrayReader.readAsArrayBuffer(file);
ArrayReader.onload = function (evt) {
var buffer = evt.target.result;
var passwordNeeded = function () {
self.display_alert(_t("You can not upload password protected file."));
self.reset_file();
self.$('.save').button('reset');
};
PDFJS.getDocument(new Uint8Array(buffer), null, passwordNeeded).then(function getPdf(pdf) {
pdf.getPage(1).then(function getFirstPage(page) {
var scale = 1;
var viewport = page.getViewport(scale);
var canvas = document.getElementById('data_canvas');
var context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
// Render PDF page into canvas context
page.render({
canvasContext: context,
viewport: viewport
}).then(function () {
var image_data = self.$('#data_canvas')[0].toDataURL();
self.$("#slide-image").attr("src", image_data);
if (loaded) {
self.$('.save').button('reset');
}
loaded = true;
});
});
var maxPages = pdf.pdfInfo.numPages;
var page, j;
self.index_content = "";
for (j = 1; j <= maxPages; j += 1) {
page = pdf.getPage(j);
page.then(function (page_obj) {
var page_number = page_obj.pageIndex + 1;
page_obj.getTextContent().then(function (data) {
var page_content = '';
_.each(data.items, function (obj) {
page_content = page_content + obj.str + " ";
});
self.index_content = self.index_content + page_number + ". " + page_content + '\n';
if (maxPages === page_number) {
if (loaded) {
self.$('.save').button('reset');
}
loaded = true;
}
});
});
}
});
};
}
var input = file.name;
var input_val = input.substr(0, input.lastIndexOf('.')) || input;
this.check_unique_slide(input_val).then(function (exist) {
if (exist) {
var message = _t("Channel contains the given title, please change before Save or Publish.");
self.display_alert(message);
}
self.$('#name').val(input_val);
});
},
reset_file: function () {
var control = this.$('#upload');
control.replaceWith(control = control.clone(true));
this.file.name = false;
},
display_alert: function (message) {
this.$('.alert-warning').remove();
$('<div class="alert alert-warning" role="alert">' + message + '</div>').insertBefore(this.$('form'));
},
/**
Wrapper for select2 load data from server at once and store it.
@param {String} Placeholder for element.
@param {bool} true for multiple selection box, false for single selection
@param {Function} Function to fetch data from remote location should return $.deferred object
resolved data should be array of object with id and name. eg. [{'id': id, 'name': 'text'}, ...]
@returns {Object} select2 wrapper object
*/
select2_wrapper: function (tag, multi, fetch_fnc) {
return {
width: '100%',
placeholder: tag,
allowClear: true,
formatNoMatches: _.str.sprintf(_t("No matches found. Type to create new %s"), tag),
multiple: multi,
selection_data: false,
fetch_rpc_fnc : fetch_fnc,
formatSelection: function (data) {
if (data.tag) {
data.text = data.tag;
}
return data.text;
},
createSearchChoice: function (term) {
return {
id: _.uniqueId('tag_'),
create: true,
tag: term,
text: _.str.sprintf(_t("Create New %s '%s'"), tag, term)
};
},
fill_data: function (query, data) {
var that = this,
tags = {results: []};
_.each(data, function (obj) {
if (that.matcher(query.term, obj.name)) {
tags.results.push({id: obj.id, text: obj.name });
}
});
query.callback(tags);
},
query: function (query) {
var that = this;
// fetch data only once and store it
if (!this.selection_data) {
this.fetch_rpc_fnc().then(function (data) {
that.fill_data(query, data);
that.selection_data = data;
});
} else {
this.fill_data(query, this.selection_data);
}
}
};
},
// Category management from select2
set_category_id: function () {
var self = this;
$('#category_id').select2(this.select2_wrapper(_t('Category'), false,
function () {
return openerp.jsonRpc("/web/dataset/call_kw", 'call', {
model: 'slide.category',
method: 'search_read',
args: [],
kwargs: {
fields: ['name'],
domain: [['channel_id', '=', self.channel_id]],
context: website.get_context()
}
});
}));
},
get_category_id: function () {
var value = $('#category_id').select2('data');
if (value && value.create) {
return [0, {'name': value.text}];
}
return [value ? value.id : null];
},
// Tags management from select2
set_tag_ids: function () {
$('#tag_ids').select2(this.select2_wrapper(_t('Tags'), true, function () {
return openerp.jsonRpc("/web/dataset/call_kw", 'call', {
model: 'slide.tag',
method: 'search_read',
args: [],
kwargs: {
fields: ['name'],
context: website.get_context()
}
});
}));
},
get_tag_ids: function () {
var res = [];
_.each($('#tag_ids').select2('data'),
function (val) {
if (val.create) {
res.push([0, 0, {'name': val.text}]);
} else {
res.push([4, val.id]);
}
});
return res;
},
// Values and save
get_value: function () {
var canvas = this.$('#data_canvas')[0],
values = {
'channel_id': this.channel_id || '',
'name': this.$('#name').val(),
'url': this.$('#url').val(),
'description': this.$('#description').val(),
'tag_ids': this.get_tag_ids(),
'category_id': this.get_category_id()
};
if (this.file.type === 'application/pdf') {
_.extend(values, {
'image': canvas.toDataURL().split(',')[1],
'index_content': this.index_content,
'slide_type': canvas.height > canvas.width ? 'document' : 'presentation',
'mime_type': this.file.type,
'datas': this.file.data
});
}
if (/^image\/.*/.test(this.file.type)) {
_.extend(values, {
'slide_type': 'infographic',
'mime_type': this.file.type,
'datas': this.file.data
});
}
return values;
},
validate: function () {
this.$('.form-group').removeClass('has-error');
if (!this.$('#name').val()) {
this.$('#name').closest('.form-group').addClass('has-error');
return false;
}
var url = this.$('#url').val() ? this.is_valid_url : false;
if (!(this.file.name || url)) {
this.$('#url').closest('.form-group').addClass('has-error');
return false;
}
return true;
},
save: function (ev) {
var self = this;
if (this.validate()) {
var values = this.get_value();
if ($(ev.target).data('published')) {
values.website_published = true;
}
this.$('.oe_slides_upload_loading').show();
this.$('.modal-footer, .modal-body').hide();
openerp.jsonRpc("/slides/add_slide", 'call', values).then(function (data) {
if (data.error) {
self.display_alert(data.error);
self.$('.oe_slides_upload_loading').hide();
self.$('.modal-footer, .modal-body').show();
} else {
window.location = data.url;
}
});
}
},
cancel: function () {
this.trigger("cancel");
}
});
// bind the event to the button
$('.oe_slide_js_upload').on('click', function () {
var channel_id = $(this).attr('channel_id');
website.slide.page_widgets['upload_dialog'] = new website.slide.Dialog(this, channel_id).appendTo(document.body);
});
});
@@ -0,0 +1,156 @@
.define-color-if-not-defined() {
@nav-tabs-border-color: #dddddd;
@brand-info: #dddddd;
@body-bg: #FFFFFF;
@brand-primary: #428bca;
@link-color: @brand-primary;
@panel-footer-bg: #f5f5f5;
@nav-link-hover-bg: #FFFFFF;
@navbar-default-link-color: #777;
@navbar-default-link-hover-color: #333;
@navbar-default-bg: #f8f8f8;
} .define-color-if-not-defined();
.oe_slides_panel_footer {
background-color: @navbar-default-bg;
}
.oe_slides_panel_footer .fa, .oe_slide_js_embed_option_link{
color : @brand-primary;
cursor: pointer;
}
.oe_slide_embed_option {
position: absolute;
padding: 30px 30px 30px 30px;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: @body-bg;
opacity: 1;
display:none;
overflow-y: auto !important;
}
.oe_slide_embed_wrapper {
position: relative;
}
section.oe_slides_promote_box {
background-color: @panel-footer-bg;
border-bottom: 1px solid @nav-tabs-border-color;
border-top: 1px solid @nav-tabs-border-color;
}
img.oe_slides_channel_thumbnail {
height: 250px;
width: 100%
}
img.oe_slides_opacity {
opacity: .5;
}
img.oe_slides_promote_image {
max-height: 300px;
width: 100%;
overflow: hidden;
display: block;
}
.oe_slides_box_shadow {
box-shadow: 0 0 5px #CCC;
}
.oe_slides_apart_small {
height: 60px;
width: 90px;
}
.oe_slides_statistics_title {
border-bottom: 1px solid @nav-tabs-border-color;
padding-bottom: 5px;
}
.oe_slides_transcript {
overflow-y: auto;
max-height: 500px;
}
.oe_slides_bottom_border {
border-bottom: 1px solid @nav-tabs-border-color;
}
.oe_slide_js_like, .oe_slide_js_unlike{
cursor: pointer;
color: @link-color;
}
// Thumbnails
.oe_slides_thumbnail_container{
height: 300px
}
.oe_slides_grid_thumbnail {
height: 200px;
width: 100%;
overflow: hidden;
}
.oe_slides_ellipsis {
white-space: nowrap;
width: 100%;
overflow: hidden;
text-overflow: ellipsis
}
// Suggested slides grid
.oe_slides_suggestion_caption {
position:absolute;
top:0;
right:0;
background:rgba(10, 10, 10, 0.75);
width:100%;
height:100%;
display: none;
text-align:center;
color:#fff !important;
z-index:2;
}
.oe_slides_suggestion_media {
margin-top: 10px;
position: relative;
}
.oe_slides_suggestion_media img {
height: 33%;
width: 100%;
overflow: hidden;
}
// PDF Viewer Loader
.oe_slides_loader{
position: relative;
width: 100%; /* for IE 6 */
}
.oe_slides_loader h4{
position: absolute;
bottom: 20px;
left: 0;
width: 35%;
color: white;
background: rgb(0, 0, 0); /* fallback color */
background: rgba(0, 0, 0, 0.7);
padding: 10px;
}
.oe_slides_panel{
padding-bottom: 50px;
height:100%;
margin-bottom:0px;
}
// PDF Viewer Share bar
.oe_slides_share_bar{
padding: 10px 0;
}
@@ -0,0 +1,75 @@
<templates id="template" xml:space="preserve">
<t t-name="website.slide.upload">
<div class="modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 class="modal-title">Upload Presentation</h3>
</div>
<div class="oe_slides_upload_loading text-center" style="display:none">
<h4><i class='fa fa-spinner fa-spin'></i> Uploading presentation... </h4>
</div>
<div class="modal-body">
<form class="clearfix form-horizontal">
<div class="form-group">
<div class="col-sm-4">
<img src="/website_slides/static/src/img/document.png" id="slide-image" title="Content Preview" class="img-responsive img-thumbnail"/>
</div>
<div class="col-sm-8">
<ul class="list-group">
<li class="list-group-item active">
<h5 class="list-group-item-heading">
<label for="upload" class="control-label">PDF or Image File</label>
</h5>
<input id="upload" name="file" class="form-control" accept="image/*,application/pdf" type="file"/>
</li>
<li class="list-group-item">
<h5 class="list-group-item-heading">
<label for="url" class="control-label">Youtube or Google Doc URL</label>
</h5>
<input id="url" name="url" class="form-control" placeholder="Youtube Video URL"/>
</li>
</ul>
</div>
</div>
<div class="form-group">
<label for="name" class="control-label col-sm-2">Title</label>
<div class="col-sm-10">
<input id="name" placeholder="Title" class="form-control" required="required"/>
</div>
</div>
<div class="form-group">
<label for="tag_ids" class="control-label col-sm-2">Tags</label>
<div class="controls col-sm-10">
<input id="tag_ids" type="hidden"/>
</div>
</div>
<div class="form-group">
<label for="category_id" class="control-label col-sm-2">Category</label>
<div class="controls col-sm-10">
<input id="category_id" type="hidden"/>
</div>
</div>
<div class="form-group">
<label for="description" class="control-label col-sm-2">Description</label>
<div class="controls col-sm-10">
<textarea id="description" rows="4" placeholder="Description" class="form-control"></textarea>
</div>
</div>
<canvas id="data_canvas" style="display: none;"></canvas>
</form>
</div>
<div class="modal-footer">
<button type="button" data-loading-text="Loading..." class="btn btn-primary save">Save as Draft</button>
<button type="button" data-loading-text="Loading..." data-published="true" class="btn btn-primary save">Save and Publish</button>
or
<a href="#" data-dismiss="modal" aria-hidden="true">Discard</a>
</div>
</div>
</div>
</div>
</t>
</templates>
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_website_config_settings" model="ir.ui.view">
<field name="name">Website livechat settings</field>
<field name="model">website.config.settings</field>
<field name="inherit_id" ref="website.view_website_config_settings"/>
<field name="arch" type="xml">
<xpath expr="//group[@string='Social Media']" position="inside">
<group string="Website Slides">
<field name="website_slide_google_app_key" />
</group>
</xpath>
</field>
</record>
</data>
</openerp>
@@ -0,0 +1,781 @@
<?xml version="1.0" ?>
<openerp>
<data>
<!-- add link to the standard footer -->
<template id="slide_footer" inherit_id="website.footer_default" name="Footer Slides Link">
<xpath expr="//div[@id='info']//ul[@class='list-unstyled']" position="inside">
<li>
<a href="/slides">Presentations</a>
</li>
</xpath>
</template>
<!-- assets -->
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Slide">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/web/static/lib/select2/select2.css"/>
<link rel="stylesheet" href="/website/static/lib/select2-bootstrap-css/select2-bootstrap.css"/>
<link type="text/less" href="/website_slides/static/src/less/website_slides.less" rel="stylesheet" t-ignore="true"/>
<!-- Js widgets, ... order loading files is IMPORTANT -->
<script type="text/javascript" src="/web/static/lib/select2/select2.js"></script>
<script type="text/javascript" src="/website_slides/static/src/js/slides.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_upload.js"/>
</xpath>
</template>
<!-- Tools template: share on social networkds -->
<template id='slides_share' name="Slides Media Share">
<div class="form-group">
<a target="_Blank" t-attf-href="https://www.facebook.com/sharer/sharer.php?u=#{slide.share_url}">
<i class="fa fa-facebook-square fa-2x"></i>
</a>
<a target="_Blank" t-attf-href="https://twitter.com/intent/tweet?text=#{slide.name}&amp;url=#{slide.share_url}">
<i class="fa fa-twitter-square fa-2x"></i>
</a>
<a target="_Blank" t-attf-href="http://www.linkedin.com/shareArticle?mini=true&amp;url=#{slide.share_url}&amp;title=#{slide.name}&amp;">
<i class="fa fa-linkedin-square fa-2x"></i>
</a>
<a target="_Blank" t-attf-href="https://plus.google.com/share?url=#{slide.share_url}">
<i class="fa fa-google-plus-square fa-2x"></i>
</a>
</div>
</template>
<!-- Channels View: main template, display list of available channels -->
<template id="channels" name="Channels">
<t t-call="website.layout">
<div class="container mt16">
<h1>Select a Channel</h1>
<div class="row">
<div t-foreach="channels" t-as="channel" class="col-sm-4 mb32">
<div class="well well-sm">
<div>
<a t-attf-href="/slides/#{slug(channel)}">
<img t-if="channel.promoted_slide_id" t-attf-src="/website/image/slide.slide/#{channel.promoted_slide_id.id}/image_medium" class="img-responsive oe_slides_channel_thumbnail"/>
<img t-if="not channel.promoted_slide_id" src="/website_slides/static/description/channel-default.jpg" class="img-responsive oe_slides_opacity"/>
</a>
</div>
<div>
<div class="mt4 pull-right">
<t t-call="website.publish_management">
<t t-set="object" t-value="channel"/>
<t t-set="publish_edit" t-value="True"/>
</t>
</div>
<h3 class="mt4 mb4 lead" t-att-title="channel.name" style="width:55%">
<a t-attf-href="/slides/#{slug(channel)}" t-esc="channel.name"/>
</h3>
<span class="label label-warning" t-if="not channel.can_see_full">Private channel</span>
</div>
<div>
<p class="text-muted oe_no_empty" t-field="channel.description"/>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
<!-- Channels View: option: subscription -->
<template id="opt_subscription_on_channels" name="Subscription" customize_show="True" inherit_id="website_slides.channels">
<xpath expr="//p[@t-field='channel.description']" position="after">
<t t-call="website_mail.follow">
<t t-set="email" t-value="user.email"/>
<t t-set="object" t-value="channel"/>
</t>
</xpath>
</template>
<!-- Channels: no channel found -->
<template id="channel_not_found" name="Private Presentation">
<t t-call="website.layout">
<div class="container">
<div class="oe_structure">
<h2>No channel created or published yet.</h2>
</div>
</div>
</t>
</template>
<!-- Channels sub-template: header -->
<template id="slides_channel_header" name="Slides Channel Header">
<section class="oe_slides_box_shadow">
<div class="container">
<div class="row">
<!-- Name and breadcrumb -->
<div class="pull-left col-md-8 col-sm-12 col-xs-12 mt16 mb8">
<h3 class="mt0">
<a t-attf-href="/slides/#{slug(channel)}" t-esc="channel.name"/>
<t t-if="category">
/ <a t-attf-href="/slides/#{slug(channel)}/category/#{slug(category)}" t-esc="category.name"/>
</t>
<!-- TDE: CLEAN THAT BROL -->
<t t-if="channel.can_upload and is_public_user">
<a t-attf-href="/web?redirect=#{request.httprequest.url}" title="Upload Presentation" class="btn btn-primary">
<i class="fa fa-cloud-upload"></i> Upload
</a>
</t>
<t t-if="channel.can_upload and not is_public_user">
<a class="btn btn-primary oe_slide_js_upload" title="Upload Presentation" t-attf-channel_id="#{channel.id}">
<i class="fa fa-cloud-upload"></i> Upload
</a>
</t>
</h3>
</div>
<!-- Search box -->
<div class="col-md-4 col-sm-12 col-xs-12 pul-right mt16 mb8">
<form t-attf-action="/slides/#{slug(channel)}" role="search" method="get">
<div class="input-group">
<input type="text" class="form-control" name="search" t-attf-placeholder="Search in #{channel.name}" t-att-value="search"/>
<span class="input-group-btn">
<button class="btn btn-primary" type="submit">
<i class="fa fa-search"></i>
</button>
</span>
</div>
</form>
</div>
</div>
<div class="row" t-if="not search">
<!-- Navigation -->
<div class="col-md-8 col-sm-12 col-xs-12">
<ul class="nav nav-tabs nav-tabs-border">
<li t-attf-class="#{not slide_type and 'active' ''}">
<a t-attf-href="/slides/#{slug(channel)}#{category and '/category/' + slug(category) or ''}"> <i class="fa fa-home"></i> Home</a>
</li>
<li t-if="header_object.nbr_presentations" t-attf-class="#{slide_type == 'presentation' and 'active' ''}">
<a t-attf-href="/slides/#{slug(channel)}#{category and '/category/' + slug(category) or ''}/presentation">
<span class="badge" t-esc="header_object.nbr_presentations"/> Presentations
</a>
</li>
<li t-if="header_object.nbr_videos" t-attf-class="#{slide_type == 'video' and 'active' ''}">
<a t-attf-href="/slides/#{slug(channel)}#{category and '/category/' + slug(category) or ''}/video">
<span class="badge" t-esc="header_object.nbr_videos"/> Videos
</a>
</li>
<li t-if="header_object.nbr_documents" t-attf-class="li #{slide_type == 'document' and 'active' ''}">
<a t-attf-href="/slides/#{slug(channel)}#{category and '/category/' + slug(category) or ''}/document">
<span class="badge" t-esc="header_object.nbr_documents"/> Documents
</a>
</li>
<li t-if="header_object.nbr_infographics" t-attf-class="#{slide_type == 'infographic' and 'active' ''}">
<a t-attf-href="/slides/#{slug(channel)}#{category and '/category/' + slug(category) or ''}/infographic">
<span class="badge" t-esc="header_object.nbr_infographics"/> Infographics
</a>
</li>
</ul>
</div>
<!-- Sort options -->
<div class="col-md-4 col-sm-12 col-xs-12">
<span class="pull-right">
<span class="text-muted">Sort by:</span>
<a href="?sorting=date">
<span t-attf-style="#{sorting and sorting == 'date' and 'font-weight:bold;' ''}">Newest</span>
</a>
<span class="text-muted">|</span>
<a href="?sorting=view">
<span t-attf-style="#{sorting and sorting == 'view' and 'font-weight:bold;' ''}">Most Viewed</span>
</a>
<span class="text-muted">|</span>
<a href="?sorting=vote">
<span t-attf-style="#{sorting and sorting == 'vote' and 'font-weight:bold;' ''}">Most Voted</span>
</a>
</span>
</div>
</div>
</div>
</section>
</template>
<!-- Channel main template -->
<template id='home' name="Odoo Slides">
<t t-set="head">
<t t-call-assets="website_slides.pdf_js_lib" t-css="false"/>
</t>
<t t-call="website.layout">
<div class="wrap">
<t t-set="main_object" t-value="channel"/>
<t t-call="website_slides.slides_channel_header">
<t t-if="not category">
<t t-set="header_object" t-value="channel"/>
</t>
<t t-if="category">
<t t-set="header_object" t-value="category"/>
</t>
</t>
<section t-if="not channel.promoted_slide_id and not tag and not slide_type and not search and not is_public_user and not category" class="wrap mt16 mb0">
<div class="container">
<div class="alert alert-danger alert-dismissable mb0">
<p>
Review your channel settings to promote your most viewed or recent published presentation
</p>
<a t-attf-href="/web#return_label=Website&amp;id=#{channel.id}&amp;view_type=form&amp;model=slide.channel" class="fa fa-arrow-right mt8">
Channel Settings
</a>
</div>
</div>
</section>
<section class="oe_slides_promote_box" t-if="channel.promoted_slide_id and not tag and not slide_type and not search and not category">
<div class="container">
<div class="row">
<div class="col-md-5 mt16 mb16">
<a t-attf-href="/slides/slide/#{slug(channel.promoted_slide_id)}">
<img t-attf-src="/website/image/slide.slide/#{channel.promoted_slide_id.id}/image" class="img-responsive shadow oe_slides_promote_image"/>
<span t-if="channel.promote_strategy in ['latest', 'most_viewed', 'most_voted', 'custom']"
class="label label-primary pull-right" style="position: absolute;right: 50px;top: 7px;"
t-field="channel.promote_strategy"/>
</a>
</div>
<div class="col-md-7 mt16 mb16">
<h3 class="row">
<a t-attf-href="/slides/slide/#{slug(channel.promoted_slide_id)}">
<t t-esc="channel.promoted_slide_id.name"/>
</a>
</h3>
<div class="row mt8 mb8" t-if="channel.promoted_slide_id.tag_ids">
<t t-foreach="channel.promoted_slide_id.tag_ids" t-as="slide_tag">
<a t-attf-href="/slides/#{slug(channel)}/tag/#{slug(slide_tag)}">
<span t-attf-class="label label-default #{tag and tag.id == slide_tag.id and 'label-primary' ''}" t-field="slide_tag.name"/>
</a>
</t>
</div>
<div class="row oe_no_empty" t-esc="channel.promoted_slide_id.description"/>
<p class="row mt8">
<b>Share:</b>
<t t-call="website_slides.slides_share">
<t t-set="slide" t-value="channel.promoted_slide_id"/>
</t>
</p>
</div>
</div>
</div>
</section>
<section>
<div class="container mt16">
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="padding-left: 15px;padding-right: 15px;" t-if="channel.total == 0">
<div class="jumbotron">
<h1>No presentation published yet.</h1>
<p>
The <i><t t-esc="channel.name"/></i> is empty.
<span t-if="can_upload">
Upload PDF presentations, documents, videos or infographic using the button below.
</span>
</p>
<p>
<a t-if="can_upload and is_public_user" t-attf-href="/web?redirect=#{request.httprequest.url}" class="btn btn-primary">
<i class="fa fa-cloud-upload"></i> Upload
</a>
<a t-if="can_upload and not is_public_user" class="btn btn-primary oe_slide_js_upload" t-attf-channel_id="#{channel.id}">
<i class="fa fa-cloud-upload"></i> Upload
</a>
</p>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" t-if="slide_type or category or tag">
<t t-call="website_slides.slides_grid_view"/>
</div>
<div class="row text-center" t-if="slide_type or category or tag">
<t t-call="website.pager"/>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 mb16" t-if="not slide_type and category_datas and not tag">
<t t-foreach="category_datas" t-as="category">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 mt8 oe_slides_bottom_border">
<a t-if="category['id']" t-attf-href="/slides/#{slug(channel)}/category/#{category['id']}" class="h3 mt0 mb0">
<t t-esc="category['name']"/>
</a>
<span t-if="not category['id']" title="Uncategorized presentation" class="h3 mt0 mb0">
<t t-esc="category['name']"/>
</span>
<a t-attf-href="/slides/#{slug(channel)}/category/#{category['id']}" class="pull-right" t-if="category['total'] - 4 > 0 and category['id']">
<i class="fa fa-arrow-right"></i> See all
</a>
</div>
</div>
<t t-call="website_slides.slides_grid_view">
<t t-set="slides" t-value="category['slides']"/>
</t>
</t>
</div>
</div>
</div>
</section>
</div>
</t>
</template>
<!-- Channels: search result -->
<template id='slides_search' name="Searching Slides">
<t t-call="website.layout">
<div class="wrap">
<t t-set="main_object" t-value="channel"/>
<t t-call="website_slides.slides_channel_header"/>
<section>
<div class="container mt16" t-if="not slides">
<div class="alert alert-danger" role="alert">
Sorry, but nothing matches your search criteria <b><t t-esc="search"/></b>. Please try again with different keywords.
</div>
</div>
<div class="container mt16" t-if="slides">
<div class="alert alert-success mb0" role="alert">
<b t-esc="len(slides)"/> results found for the given criteria <b><t t-esc="search"/></b>
</div>
<div class="row mt0">
<t t-call="website_slides.slides_grid_view"/>
</div>
<div class="row text-center">
<t t-call="website.pager"/>
</div>
</div>
</section>
</div>
</t>
</template>
<!-- Embedded template: display a list of slides in a grid view -->
<template id='slides_grid_view' name="Slides (Grid View)">
<div class="col-lg-3 col-md-4 col-sm-6 col-xs-12 mt16 mb16" t-foreach="slides" t-as="slide">
<div class="well well-sm oe_slides_thumbnail_container">
<a t-attf-href="/slides/slide/#{slug(slide)}">
<img t-attf-src="/website/image/slide.slide/#{slide.id}/image_thumb" class="img-responsive img-thumbnail oe_slides_grid_thumbnail"/>
</a>
<span t-if="not slide.website_published" class="label label-danger" style="position: absolute;right: 20px;top: 7px;">Not Published</span>
<a t-if="not is_public_user and slide.website_published and slide.channel_id.promote_strategy == 'custom' and slide.channel_id.promoted_slide_id.id != slide.id"
t-attf-href="/slides/slide/#{slide.id}/promote"
style="position: absolute;right: 20px;top: 7px;"
t-attf-title="Promote this #{slide.slide_type}">
<span class="fa-stack fa-lg">
<i class="fa fa-square fa-stack-2x oe_slides_opacity"></i>
<i class="fa fa-bullhorn fa-stack-1x fa-inverse"></i>
</span>
</a>
<div style="padding: 5px;">
<h4 class="mt4 mb8 oe_slides_ellipsis">
<a t-att-title="slide.name" t-attf-href="/slides/slide/#{slug(slide)}" t-esc="slide.name"/>
</h4>
<div class="text-muted clearfix">
<small class="pull-left"><t t-esc="slide.total_views"/> Views</small>
<small class="pull-right"><timeago class="timeago" t-att-datetime="slide.create_date"></timeago></small>
</div>
<p t-if="slide.tag_ids" style="padding-top: 5px;">
<t t-foreach="slide.tag_ids" t-as="att_tag">
<a t-attf-href="/slides/#{slug(channel)}/tag/#{slug(att_tag)}">
<span t-attf-class="label label-default #{tag and tag.name == att_tag.name and 'label-primary' ''} text-muted" t-field="att_tag.name"/>
</a>
</t>
</p>
</div>
</div>
</div>
</template>
<!-- Slide: main template: detailed view -->
<template id="slide_detail_view" name="Slide Detailed View">
<t t-set="head">
<t t-if="not private">
<meta property="og:title" t-att-content="slide.name" />
<meta property="og:type" content="website" />
<meta property="og:url" t-att-content="slide.share_url" />
<meta property="og:image" t-attf-content="#{request.httprequest.host_url}/website/image/slide.slide/#{slide.id}/image_thumb"/>
<meta property="og:description" t-att-content="slide.description" />
</t>
</t>
<t t-call="website.layout">
<t t-set="main_object" t-value="slide"/>
<div class="container mt16">
<div class="row">
<div class="col-lg-8 col-md-8 col-sm-12 col-xs-12">
<div t-if="private" class="mb16">
<h3 class="alert alert-danger">
<i class="fa fa-exclamation-triangle"></i> This <t t-esc="slide.slide_type"/> is private
</h3>
<t t-if="slide.channel_id.access_error_msg">
<div t-field="slide.channel_id.access_error_msg" class="well oe_no_empty"/>
</t>
</div>
<div t-if="not private">
<t t-if="slide.datas and slide.slide_type == 'infographic'">
<img t-attf-src="/website/image/slide.slide/#{slide.id}/datas" class="img-responsive" style="width:100%"/>
</t>
<div t-if="slide.slide_type in ('presentation', 'document')" class="embed-responsive embed-responsive-16by9 embed-responsive-item mb8">
<t t-raw="slide.embed_code"/>
</div>
<div t-if="slide.slide_type == 'video' and slide.document_id" class="embed-responsive embed-responsive-16by9 embed-responsive-item mb8">
<t t-raw="slide.embed_code"/>
</div>
<div class="row">
<div class="pull right">
<t t-call="website.publish_management">
<t t-set="object" t-value="slide"/>
<t t-set="publish_edit" t-value="True"/>
</t>
</div>
<h4 t-esc="slide.name" class="col-md-8"/>
<div class="col-md-4" t-if="slide.tag_ids">
<t t-foreach="slide.tag_ids" t-as="slide_tag">
<a t-attf-href="/slides/#{slug(slide.channel_id)}/tag/#{slug(slide_tag)}">
<span t-attf-class="label label-default #{tag and tag.id == slide_tag.id and 'label-primary' ''}" t-field="slide_tag.name"/>
</a>
</t>
</div>
</div>
<div class="row">
<div class="col-sm-8 col-xs-12">
<div class="pull-left">
<a t-attf-href="/slides/#{slug(slide.channel_id)}">
<i class="fa fa-circle-o fa-5x" style="color: #a24689;font-weight: bold;"></i>
</a>
</div>
<div style="margin-left: 80px">
<p>
<a t-attf-href="/slides/#{slug(slide.channel_id)}" t-esc="slide.channel_id.name"/>
<t t-if="slide and slide.category_id">
<span> / </span>
<a t-attf-href="/slides/#{slug(slide.channel_id)}/category/#{slug(slide.category_id)}" t-esc="slide.category_id.name"/>
</t>
</p>
<div>
<t t-call="website_mail.follow">
<t t-set="email" t-value="user.email"/>
<t t-set="object" t-value="slide.channel_id"/>
</t>
</div>
</div>
</div>
<div class="col-sm-4 hidden-xs">
<div class="text-right">
<b class="h3 text-muted"><t t-esc="slide.total_views"/></b> <span class="text-muted small">views</span>
</div>
<!-- progress bar of like/dislike -->
<div class="progress mb0" style="height: 2px;" t-if="slide.likes + slide.dislikes > 0">
<div class="progress-bar" role="progressbar" t-attf-aria-valuenow="#{slide.likes}" aria-valuemin="0"
t-attf-aria-valuemax="#{slide.likes + slide.dislikes}"
t-attf-style="width: #{(slide.likes*100)/(slide.likes + slide.dislikes)}%;">
<span class="sr-only"><t t-esc="slide.likes"/> Likes</span>
</div>
</div>
<div class="progress mb0" style="height: 2px;" t-if="slide.likes + slide.dislikes == 0">
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
<span class="sr-only"><t t-esc="slide.likes"/> Likes</span>
</div>
</div>
<!-- LikeButton widget -->
<div class="text-muted mt4">
<div class="pull-right mb16 text-right">
<span
class="oe_slide_js_like"
data-href="/slides/slide/like"
t-att-data-user-id="user.id"
t-att-data-public-user="is_public_user"
t-att-data-slide-id="slide.id"
data-toggle="popover">
<i class="fa fa-thumbs-up fa-1x">
<t t-esc="slide.likes"/>
</i>
</span>
<span
class="oe_slide_js_unlike"
data-href="/slides/slide/dislike"
t-att-data-user-id="user.id"
t-att-data-public-user="is_public_user"
t-att-data-slide-id="slide.id"
data-toggle="popover">
<i class="fa fa-thumbs-down fa-1x">
<t t-esc="slide.dislikes"/>
</i>
</span>
</div>
</div>
</div>
</div>
<div class="mt8 mb32">
<ul class="nav nav-tabs" role="tablist">
<li t-att-class="not comments and 'active' or ''">
<a href="#about" role="tab" data-toggle="tab">
<i class="fa fa-home"></i> About
</a>
</li>
<li>
<a href="#share" role="tab" data-toggle="tab">
<i class="fa fa-share-alt"></i> Share
</a>
</li>
<li t-att-class="comments and 'active' or ''">
<a href="#discuss" role="tab" data-toggle="tab">
<i class="fa fa-comments-o"></i> Comments
</a>
</li>
<li>
<a href="#transcript" role="tab" data-toggle="tab">
<i class="fa fa-align-justify"></i> Transcript
</a>
</li>
<li>
<a href="#statistic" role="tab" data-toggle="tab">
<i class="fa fa-bar-chart"></i> Statistics
</a>
</li>
</ul>
<div class="tab-content" style="padding: 20px 5px 5px 5px; word-wrap: break-word;">
<div t-att-class="not comments and 'tab-pane fade active' or 'tab-pane fade'" id="about">
<div t-field="slide.description"/>
</div>
<div class="tab-pane fade" t-if="slide.website_published" id="share">
<t t-call="website_slides.slide_social_media">
<t t-set="slide" t-value="slide"/>
</t>
<t t-call="website_slides.slide_social_email">
<t t-set="slide" t-value="slide"/>
</t>
<t t-if="not slide.document_id">
<t t-call="website_slides.slide_social_embed">
<t t-set="slide" t-value="slide"/>
</t>
</t>
</div>
<div class="tab-pane fade" t-if="not slide.website_published" id="share">
<h4><i class="fa fa-info-circle"></i>
Publish a presentation to enable social sharing feature
</h4>
</div>
<div t-att-class="comments and 'tab-pane fade active' or 'tab-pane fade'" id="discuss">
<form id="comment" t-attf-action="/slides/slide/#{slide.id}/comment" method="POST">
<div class="media">
<span class="pull-left">
<img class="img img-circle media-object"
t-att-src="'/website/image?model=res.partner&amp;field=image_small&amp;id='+str(user_id.partner_id.id)"
style="width: 30px"/>
</span>
<div class="media-body">
<div t-if="is_public_user" class="col-md-6">
<input type="name" class="form-control mb8" name="name" placeholder="Your Name"/>
<input type="email" class="form-control mb8" name="email" placeholder="Email Address"/>
</div>
<div t-if="is_public_user" class="col-md-6">
<p class="text-muted small">
Post comment with email address (confirmation of email is required in order to publish comment on website) or please
<a t-attf-href="/web?redirect=#{request.httprequest.url}">login</a>
to post comment
</p>
</div>
<textarea rows="3" name="comment" class="form-control" placeholder="Write a comment..."></textarea>
<button type="submit" class="btn btn-primary mt8">Post</button>
</div>
</div>
</form>
<ul class="media-list mt8">
<li t-foreach="comments" t-as="message" class="media">
<span class="pull-left">
<img class="media-object img img-circle"
t-att-src="'/website/image?model=mail.message&amp;field=author_avatar&amp;id='+str(message.id)" style="width: 30px"/>
</span>
<div class="media-body">
<t t-call="website.publish_short">
<t t-set="object" t-value="message"/>
</t>
<span t-field="message.author_id" style="display: inline-block;" t-field-options='{"widget": "contact", "fields": ["name"]}'/>
<span class="text-muted">on<span t-field="message.date"/></span>
<div t-field="message.body" class="discussion_scroll_post"></div>
</div>
</li>
</ul>
</div>
<div class="tab-pane fade oe_slides_transcript" id="transcript">
<t t-if="slide.index_content">
<t t-foreach="slide.index_content.split('\n')" t-as="line">
<p t-esc="line"></p>
</t>
</t>
</div>
<div class="tab-pane fade" id="statistic">
<div class="row">
<div class="col-md-4">
<h4 class="mt0 oe_slides_statistics_title">Views</h4>
<ul class="list-group">
<li class="list-group-item">
<span class="badge"><t t-esc="slide.total_views"/></span>
<i class="fa fa-play"></i> Total Views
</li>
<li class="list-group-item">
<span class="badge"><t t-esc="slide.slide_views"/></span>
<i class="fa fa-circle-o"></i> Website Views
</li>
<li class="list-group-item" t-if="not slide.document_id">
<span class="badge"><t t-esc="slide.embed_views"/></span>
<i class="fa fa-code"></i> Embeded Views
</li>
</ul>
</div>
<div class="col-md-4">
<h4 class="mt0 oe_slides_statistics_title">Actions</h4>
<ul class="list-group">
<li class="list-group-item">
<span class="badge" id="total-share">0</span>
<i class="fa fa-share-alt"></i> Social Shares
</li>
<li class="list-group-item">
<span class="badge"><t t-esc="slide.likes"/></span>
<i class="fa fa-thumbs-up"></i> Likes
</li>
<li class="list-group-item">
<span class="badge"><t t-esc="slide.dislikes"/></span>
<i class="fa fa-thumbs-down"></i> Dislikes
</li>
<li class="list-group-item">
<span class="badge"><t t-esc="len(slide.website_message_ids)"/></span>
<i class="fa fa-comments-o"></i> Comments
</li>
</ul>
</div>
<div class="col-md-4">
<h4 class="mt0 oe_slides_statistics_title">Share count</h4>
<ul class="list-group">
<li class="list-group-item">
<span class="badge" id="facebook-badge">0</span>
<i class="fa fa-facebook-square"></i> Facebook
</li>
<li class="list-group-item">
<span class="badge" id="twitter-badge">0</span>
<i class="fa fa-twitter-square"></i> Twitter
</li>
<li class="list-group-item">
<span class="badge" id="linkedin-badge">0</span>
<i class="fa fa-linkedin-square"></i> LinkedIn
</li>
<li class="list-group-item">
<span class="badge" id="google-badge">0</span>
<i class="fa fa-google-plus-square"></i> Google+
</li>
</ul>
</div>
</div>
<div class="row" t-if="not slide.document_id and slide.embedcount_ids">
<div class="col-md-12">
<h4 class="mt0"> Embeds
<span class="badge pull-right">
<t t-esc="len(slide.embedcount_ids)"/>
</span>
</h4>
<ul class="list-group" style="height: 150px;overflow: auto;">
<li class="list-group-item" t-foreach="slide.embedcount_ids" t-as="count">
<span class="badge"><t t-esc="count.count_views"/></span>
<t t-esc="count.url"/>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4 col-md-4 col-sm-12 col-xs-12">
<ul class="nav nav-tabs" role="tablist">
<li class="active"><a href="#related" data-toggle="tab">Related</a></li>
<li><a href="#most_viewed" data-toggle="tab">Most Viewed</a></li>
</ul>
<div class="tab-content">
<div id="related" class="tab-pane active">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 panel panel-default">
<ul class="media-list panel-body">
<t t-foreach="related_slides" t-as="slide">
<t t-call="website_slides.related_slides"/>
</t>
</ul>
</div>
</div>
<div id="most_viewed" class="tab-pane">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 panel panel-default">
<ul class="media-list panel-body">
<t t-foreach="most_viewed_slides" t-as="slide">
<t t-call="website_slides.related_slides"/>
</t>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
<!-- Slide sub-template: display an item in a list of related slides (Related, Most Viewed, ...) -->
<template id="related_slides" name="Related Slide">
<li class="media">
<a class="media-left pull-left" t-attf-href="/slides/slide/#{slug(slide)}">
<img class="media-object oe_slides_apart_small" t-attf-src="/website/image/slide.slide/#{slide.id}/image_thumb"/>
</a>
<div class="media-body">
<a t-attf-href="/slides/slide/#{slug(slide)}"><h5 class="media-heading" t-esc="slide.name"/></a>
<small class="text-muted">
<t t-esc="slide.total_views"/> Views . <timeago class="timeago" t-att-datetime="slide.create_date"></timeago>
</small>
</div>
</li>
</template>
<!-- Slide sub-template: share: social media -->
<template id='slide_social_media' name="Share on Social Networks">
<h4 class="mt0">Share on Social Networks</h4>
<t t-call="website_slides.slides_share"/>
<h4 class="mt0">Share Link</h4>
<input type="text" class="form-control" t-att-value="slide.share_url" readonly="readonly" onClick="this.select();"/>
<span class="help-block">Use permanent link to share in social media</span>
</template>
<!-- Slide sub-template: share: send by email -->
<template id='slide_social_email' name="Share by Email">
<h4 class="mt0">Share with a friend</h4>
<t t-if="not is_public_user">
<form class="form-group oe_slide_js_share_email" role="form">
<div class="input-group col-md-6">
<input type="email" class="form-control" placeholder="your-friend@domain.com"/>
<span class="input-group-btn">
<button class="btn btn-primary" type="button"
data-loading-text="Sending..."
t-attf-data-slide-id="#{slide.id}"
style="border-top-right-radius: 4px;border-bottom-right-radius: 4px;">
<i class="fa fa-envelope-o"></i>
Send Email
</button>
</span>
</div>
<span class="help-block">Send presentation through email</span>
</form>
</t>
<t t-if="is_public_user">
<p>Please <a t-attf-href="/web?redirect=#{request.httprequest.url}"> login </a> to send this <t t-esc="slide.slide_type"/> by email!</p>
</t>
</template>
<!-- Slide sub-template: share: embed in your website -->
<template id="slide_social_embed" name="Share on Your Website">
<div class="oe_slide_js_embed_code_widget">
<h4 class="mt0">Embed in your website</h4>
<div class="form-group">
<textarea class="form-control slide_embed_code" readonly="readonly" onClick="this.select();"><t t-esc="slide.embed_code"/></textarea>
</div>
<div class="form-group" t-if="slide.slide_type in ('presentation', 'document')">
<div class="help-block col-lg-3 col-md-3 col-sm-6 col-xs-6">Select page to start with</div>
<div class="input-group col-lg-3 col-md-3 col-sm-6 col-xs-6">
<input type="number" value="1" class="form-control"/>
</div>
</div>
</div>
</template>
</data>
</openerp>
@@ -0,0 +1,267 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<menuitem name="Marketing" id="base.marketing_menu" sequence="85" groups="base.group_user"/>
<menuitem id="menu_slides" parent="base.marketing_menu" name="Slides"/>
<!-- SLIDE.TAG -->
<record id="view_slide_tag_form" model="ir.ui.view">
<field name="name">slide.tag.form</field>
<field name="model">slide.tag</field>
<field name="arch" type="xml">
<form string="Tag">
<field name="name"/>
</form>
</field>
</record>
<record id="view_slide_tag_tree" model="ir.ui.view">
<field name="name">slide.tag.tree</field>
<field name="model">slide.tag</field>
<field name="arch" type="xml">
<tree string="Tags">
<field name="name"/>
</tree>
</field>
</record>
<record id="action_slide_tag" model="ir.actions.act_window">
<field name="name">Tags</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">slide.tag</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem action="action_slide_tag" id="menu_slide_tag" parent="menu_slides" sequence="10" groups="base.group_no_one"/>
<!-- SLIDE.SLIDE -->
<record id="view_slide_slide_form" model="ir.ui.view">
<field name="name">slide.slide.form</field>
<field name="model">slide.slide</field>
<field name="arch" type="xml">
<form string="Slide">
<sheet>
<field name="image" widget="image" class="oe_left oe_avatar" options='{"preview_image": "image_thumb"}'/>
<div class="oe_title oe_left">
<div class="oe_edit_only">
<label for="name" string="Name"/>
</div>
<h1>
<field name="name" default_focus="1" placeholder="Name"/>
</h1>
<field name="tag_ids" widget="many2many_tags" placeholder="Tags..."/>
</div>
<div class="oe_right oe_button_box" name="buttons">
<field name="website_published" class="pull-right" widget="website_button"/>
</div>
<group>
<field name="channel_id"/>
<field name="category_id" context="{'default_channel_id': channel_id}"/>
<field name="share_url"/>
</group>
<notebook>
<page string="Description">
<field name="description"/>
</page>
<page string="Document">
<group>
<field name="url"/>
<field name="slide_type" readonly="1"/>
<field name="document_id" readonly="1"/>
<field name="mime_type" readonly="1"/>
<field name="datas" readonly="1"/>
</group>
</page>
<page string="Website">
<group>
<group>
<field name="download_security"/>
<field name="date_published"/>
<field name="likes"/>
<field name="dislikes"/>
</group>
<group>
<field name="slide_views"/>
<field name="embed_views"/>
<field name="total_views"/>
</group>
</group>
<label for="index_content"/>
<field name="index_content"/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="view_slide_slide_tree" model="ir.ui.view">
<field name="name">slide.slide.tree</field>
<field name="model">slide.slide</field>
<field name="arch" type="xml">
<tree string="Slides">
<field name="name"/>
<field name="slide_type"/>
<field name="channel_id"/>
<field name="category_id"/>
<field name="date_published"/>
</tree>
</field>
</record>
<record id="view_slide_slide_search" model="ir.ui.view">
<field name="name">slide.slide.filter</field>
<field name="model">slide.slide</field>
<field name="arch" type="xml">
<search string="Search Slides">
<field name="name"/>
<filter name="published" string="Published" domain="[('website_published', '=', True)]"/>
<filter name="not_published" string="Waiting for validation" domain="[('website_published', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="Channel" domain="[]" context="{'group_by': 'channel_id'}"/>
<filter string="Category" domain="[]" context="{'group_by': 'category_id'}"/>
<filter string="Type" domain="[]" context="{'group_by': 'slide_type'}"/>
</group>
</search>
</field>
</record>
<record id="view_slide_slide_graph" model="ir.ui.view">
<field name="name">slide.slide.graph</field>
<field name="model">slide.slide</field>
<field name="arch" type="xml">
<graph string="Graph of Slides" type="pivot">
<field name="date_published" interval="month" type="col"/>
<field name="channel_id" type="row"/>
</graph>
</field>
</record>
<record id="action_slides_slides" model="ir.actions.act_window">
<field name="name">Slides</field>
<field name="res_model">slide.slide</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
</record>
<menuitem action="action_slides_slides" id="submenu_action_slides_slides" parent="menu_slides" sequence="3"/>
<!-- SLIDE.CATEGORY -->
<record model="ir.ui.view" id="view_slides_category_form">
<field name="name">slide.category.form</field>
<field name="model">slide.category</field>
<field name="arch" type="xml">
<form string="Category">
<sheet>
<group>
<field name="name"/>
<field name="channel_id"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_slides_category_tree" model="ir.ui.view">
<field name="name">slide.category.tree</field>
<field name="model">slide.category</field>
<field name="arch" type="xml">
<tree string="Category">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="channel_id"/>
</tree>
</field>
</record>
<record id="action_ir_slide_category" model="ir.actions.act_window">
<field name="name">Categories</field>
<field name="res_model">slide.category</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem action="action_ir_slide_category" id="menu_action_ir_slide_category" parent="menu_slides" sequence="2"/>
<!-- SLIDE.CHANNEL VIEWS -->
<record model="ir.ui.view" id="view_slide_channel_form">
<field name="name">slide.channel.form</field>
<field name="model">slide.channel</field>
<field name="arch" type="xml">
<form string="Channels">
<sheet>
<div class="oe_title oe_left">
<div class="oe_edit_only">
<label for="name" string="Name"/>
</div>
<h1>
<field name="name" default_focus="1" placeholder="Name"/>
</h1>
</div>
<div class="oe_right oe_button_box" name="buttons">
<field name="website_published" class="pull-right" widget="website_button"/>
</div>
<field name="category_ids" widget="many2many_tags" placeholder="Categories" context="{'default_channel_id': active_id}"
domain="[('channel_id','=', active_id)]"/>
<notebook colspan="4">
<page string="General">
<group>
<group>
<field name="promote_strategy" widget="radio"/>
<field name="custom_slide_id" class="oe_edit_only" attrs="{'invisible': [('promote_strategy', '!=', 'custom')]}" domain="[('channel_id','=',id), ('website_published','=',True)]"/>
<field name="promoted_slide_id" attrs="{'invisible': [('promote_strategy', '!=', 'custom')]}"/>
</group>
<group>
<field name="publish_template_id" domain="[('model','=','slide.slide')]"/>
<field name="share_template_id" domain="[('model','=','slide.slide')]"/>
</group>
</group>
<label for="description"/>
<field name="description" colspan="4" nolabel="1"/>
</page>
<page string="Security">
<group>
<field name="upload_group_ids" widget="many2many_tags"/>
<field name="visibility" string="Channel visibility" widget="radio"/>
<field name="group_ids" widget="many2many_tags" attrs="{'invisible':[('visibility', '=', 'public')], 'required':[('visibility', 'in',('private', 'partial'))]}"/>
</group>
<group attrs="{'invisible':[('visibility','in',('public'))]}" colspan="4">
<label string="Error Message"/>
<field name="access_error_msg" colspan="4" nolabel="1"/>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="view_slide_channel_tree" model="ir.ui.view">
<field name="name">slide.channel.tree</field>
<field name="model">slide.channel</field>
<field name="arch" type="xml">
<tree string="Channel">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="visibility"/>
<field name="promote_strategy"/>
</tree>
</field>
</record>
<record id="action_slide_channels" model="ir.actions.act_window">
<field name="name">Channels</field>
<field name="res_model">slide.channel</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem action="action_slide_channels" id="menu_action_slide_channels" parent="menu_slides" sequence="1"/>
</data>
</openerp>
@@ -0,0 +1,246 @@
<?xml version="1.0" ?>
<openerp>
<data>
<!-- Bundle (minimal) for embedded slide iframe -->
<template id="website_slides.slide_embed_assets" name="Website slides embed assets">
<link rel='stylesheet' href='/web/static/lib/fontawesome/css/font-awesome.css'/>
<link rel='stylesheet' href='/web/static/lib/bootstrap/css/bootstrap.css'/>
<link type="text/less" href="/website_slides/static/src/less/website_slides.less" rel="stylesheet" t-ignore="true"/>
<script type="text/javascript" src="/web/static/lib/jquery/jquery.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/bootstrap.js"></script>
<t t-call="website_slides.pdf_js_lib"></t>
<script type="text/javascript" src="/website_slides/static/src/js/slides_embed.js"></script>
</template>
<!--
Bundle for PDFJS lib
Since PDFJS is quite huge (40000 lines), please only load it when it is necessary.
For now, it is only use to display the PDF slide Viewer during an embed.
Bundlized, the size is reduced to 3000 lines.
-->
<template id="pdf_js_lib" name="PDF Js library">
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/shared/util.js"></script>
<!-- files required to handle PDF file. Found in https://github.com/mozilla/pdf.js/blob/master/examples/helloworld/index.html -->
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/display/api.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/display/metadata.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/display/canvas.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/display/webgl.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/display/pattern_helper.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/display/font_loader.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/display/annotation_helper.js"></script>
<!-- files normally dynamically loaded in worker_loader.js -->
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/network.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/chunked_stream.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/pdf_manager.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/core.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/obj.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/charsets.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/annotation.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/function.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/colorspace.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/crypto.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/pattern.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/evaluator.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/cmap.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/fonts.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/font_renderer.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/glyphlist.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/image.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/metrics.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/parser.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/ps_parser.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/stream.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/worker.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/arithmetic_decoder.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/jpg.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/jpx.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/jbig2.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/bidi.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/core/murmurhash3.js"></script>
<!--
NOTE JEM : PDFJS not-minified load its assets dynamically. Not minify, this lib is huge (4000 lines).
To load PDFJS using odoo bundle, we create a custom worker_loader, to avoid reloading all the assets (core/*.js) dynamically.
Loading all this assets in a bundle can be problematic in debug mode, since we don't wait all assets to be loaded to start using PDFJS. That's why, in debug mode, we use the native loader,
otherwise our custom loader is used.
-->
<t t-if="not debug">
<script>
// Use Odoo PDFJS Loader, to avoid dynamic assets loading
PDFJS.workerSrc = '/website_slides/static/lib/pdfjs/worker_loader_by_odoo.js';
</script>
</t>
<t t-if="debug">
<script>
// Specify the main script used to create a new PDF.JS web worker.
// In production, leave this undefined or change it to point to the
// combined `pdf.worker.js` file.
PDFJS.workerSrc = '/website_slides/static/lib/pdfjs/worker_loader.js';
</script>
</t>
<!--
If no loader defined, this file will try to load pdf.worker.js (in build/).
It will crash since we don't use the minified version of PDFJS
-->
<script type="text/javascript" src="/website_slides/static/lib/pdfjs/pdf.js"></script>
<script type="text/javascript" src="/website_slides/static/lib/pdfslidesviewer/PDFSlidesViewer.js"></script>
</template>
<!--
This template is the PDF Viewer. It will mostly rendered throught an iFrame.
The js file to bind event is slides_embed.js
-->
<template id="embed_slide" name="Embedded Slide Page">
<html>
<head t-if="not private">
<title><t t-esc="slide.name"/></title>
<script type="text/javascript" src="/website_slides/static/lib/pdfslidesviewer/PDFSlidesViewer.js"></script>
<t t-call-assets="website_slides.slide_embed_assets"/>
</head>
<body t-if="private" style="position: inherit;">
<title><t t-esc="slide.name"/></title>
<div class="slide-private-view">
<h3 style="border-bottom: 1px solid !important;padding-bottom: 10px;"><i class="fa fa-exclamation-triangle"></i> This <t t-esc="slide.slide_type"/> is private.</h3>
<div t-field="slide.channel_id.access_error_msg" />
</div>
</body>
<body>
<div id="PDFViewer" t-if="not private" class="panel panel-default oe_slides_panel">
<!-- PDF Viewer Header : contains the name, and the share links -->
<div t-if="is_embedded" class="navbar-default oe_slides_share_bar">
<div class="container-fluid">
<div class="row">
<div class="col-lg-8 col-md-8 col-sm-4 col-xs-4 text-left">
<div class="oe_slides_ellipsis">
<a target="_new" t-att-href="slide.share_url">
<span t-esc="slide.name" t-attf-title="#{slide.name}"/>
</a>
</div>
</div>
<div class="col-lg-1 col-md-1 col-sm-2 col-xs-2 text-right">
<b class="oe_slide_js_embed_option_link" data-slide-option-id="#slide_share"><i class="fa fa-share-alt"></i> Share</b>
</div>
<div class="col-lg-1 col-md-1 col-sm-2 col-xs-2 text-right">
<b class="oe_slide_js_embed_option_link" data-slide-option-id="#slide_email"><i class="fa fa-envelope"></i> Email</b>
</div>
<div class="col-lg-1 col-md-1 col-sm-2 col-xs-2 text-right">
<b class="oe_slide_js_embed_option_link" data-slide-option-id="#slide_embed"><i class="fa fa-code"></i> Embed</b>
</div>
</div>
</div>
</div>
<!-- PDF Viewer Body : contains the canvas, the loader, or the image-->
<div id="PDFSlideViewer" style="position:relative;"
t-attf-data-slideid="#{slide.id}"
t-attf-data-slideurl="/slides/slide/#{slide.id}/pdf_content"
t-attf-data-downloadable="#{slide.download_security}"
t-att-data-defaultpage="page">
<t t-if="is_embedded">
<div id="slide_share" class="oe_slide_embed_option">
<t t-call="website_slides.slide_social_media"/>
</div>
<div id="slide_email" class="oe_slide_embed_option">
<t t-call="website_slides.slide_social_email"/>
</div>
<div id="slide_embed" class="oe_slide_embed_option">
<t t-call="website_slides.slide_social_embed"/>
</div>
</t>
<div id="slide_suggest" class="oe_slide_embed_option">
<t t-foreach="related_slides" t-as="suggest_slide">
<div class="col-md-4 col-sm-4 col-xs-4 oe_slides_suggestion_media">
<a t-att-href="suggest_slide.share_url" target="_new">
<div class="oe_slides_suggestion_caption">
<h4><t t-esc="suggest_slide.name"/></h4>
</div>
<span class="oe-image-thumbnail">
<img class="img-responsive" t-attf-src="/website/image/slide.slide/#{suggest_slide.id}/image_thumb" alt="suggest_slide.name"/>
</span>
</a>
</div>
</t>
</div>
<t t-if="slide.slide_type in ('presentation', 'document')">
<div id="PDFViewerLoader" class="oe_slides_loader">
<img class="img-responsive" t-attf-src="/website/image/slide.slide/#{slide.id}/image_medium" style="margin:auto;height:100%"/>
<h4 class="text-center">
<i class="fa fa-spinner fa-spin"></i> Loading ...
</h4>
</div>
<canvas id="PDFViewerCanvas" class="img-responsive" style="margin:auto;height:100%;display:none;"></canvas>
</t>
<t t-if="slide.slide_type in ('infographic')">
<img t-attf-src="/website/image/slide.slide/#{slide.id}/datas" class="img-responsive" style="width:100%"/>
</t>
</div>
<!-- Fixed bottom navbar -->
<div class="navbar navbar-default navbar-fixed-bottom" role="navigation" t-if="slide.slide_type in ('presentation', 'document')">
<div class="container-fluid oe_slides_panel_footer">
<div class="row" style="padding-top:15px;">
<div class="col-lg-2 col-md-2 col-sm-2 col-xs-2 text-left">
<input type="number" class="text-center slide-img-border" style="width: 45px;" id="page_number"></input> / <span id="page_count"></span>
</div>
<div class="col-lg-8 col-md-8 col-sm-8 col-xs-8">
<div class="col-lg-4 col-md-4 col-sm-4 col-xs-4 text-center"></div>
<div class="col-lg-1 col-md-1 col-sm-1 col-xs-1 text-center">
<span id="first" title="First slide">
<i class="fa fa-step-backward fa-2x"></i>
</span>
</div>
<div class="col-lg-1 col-md-1 col-sm-1 col-xs-1 text-center">
<span id="previous" title="Previous slide" role="button">
<i class="fa fa-arrow-circle-left fa-2x"></i>
</span>
</div>
<div class="col-lg-1 col-md-1 col-sm-1 col-xs-1 text-center">
<span id="next" title="Next slide" role="button">
<i class="fa fa-arrow-circle-right fa-2x"></i>
</span>
</div>
<div class="col-lg-1 col-md-1 col-sm-1 col-xs-1 text-center">
<span id="last" title="Last slide" role="button">
<i class="fa fa-step-forward fa-2x"></i>
</span>
</div>
<div class="col-lg-4 col-md-4 col-sm-4 col-xs-4 text-center"></div>
</div>
<div class="col-lg-2 col-md-2 col-sm-2 col-xs-2 text-right">
<a t-if="slide.download_security and not is_embedded" t-attf-href="/slides/slide/#{slide.id}/download" title="Download" id="download">
<i class="fa fa-download fa-2x"></i>
</a>
<span id="fullscreen" title="View fullscreen">
<i class="fa fa-arrows-alt fa-2x"></i>
</span>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
</template>
<!--
Template render isntead of Embedded Slide, it this one is forbidden.
So, it will mostly be rendered throught an iFrame
-->
<template id="embed_slide_forbidden" name="Forbidden Embedded Slide">
<html>
<head t-if="not private">
<title><t t-esc="slide.name"/></title>
<t t-call-assets="website_slides.slide_embed_assets" t-js="false"/>
</head>
<body>
<title><t t-esc="slide.name"/></title>
<div class="slide-private-view">
<h3 style="border-bottom: 1px solid !important;padding-bottom: 10px;"><i class="fa fa-exclamation-triangle"></i> This <t t-esc="slide.slide_type"/> is private.</h3>
<div t-field="slide.channel_id.access_error_msg" />
</div>
</body>
</html>
</template>
</data>
</openerp>
+49
View File
@@ -162,6 +162,55 @@ def image_resize_image_small(base64_source, size=(64, 64), encoding='base64', fi
"""
return image_resize_image(base64_source, size, encoding, filetype, avoid_if_small)
# ----------------------------------------
# Crop Image
# ----------------------------------------
def crop_image(data, type='top', ratio=False, thumbnail_ratio=None, image_format="PNG"):
""" Used for cropping image and create thumbnail
:param data: base64 data of image.
:param type: Used for cropping position possible
Possible Values : 'top', 'center', 'bottom'
:param ratio: Cropping ratio
e.g for (4,3), (16,9), (16,10) etc
send ratio(1,1) to generate square image
:param thumbnail_ratio: It is size reduce ratio for thumbnail
e.g. thumbnail_ratio=2 will reduce your 500x500 image converted in to 250x250
:param image_format: return image format PNG,JPEG etc
"""
if not data:
return False
image_stream = Image.open(StringIO.StringIO(data.decode('base64')))
output_stream = StringIO.StringIO()
w, h = image_stream.size
new_h = h
new_w = w
if ratio:
w_ratio, h_ratio = ratio
new_h = (w * h_ratio) / w_ratio
new_w = w
if new_h > h:
new_h = h
new_w = (h * w_ratio) / h_ratio
if type == "top":
cropped_image = image_stream.crop((0, 0, new_w, new_h))
cropped_image.save(output_stream, format=image_format)
elif type == "center":
cropped_image = image_stream.crop(((w - new_w) / 2, (h - new_h) / 2, (w + new_w) / 2, (h + new_h) / 2))
cropped_image.save(output_stream, format=image_format)
elif type == "bottom":
cropped_image = image_stream.crop((0, h - new_h, new_w, h))
cropped_image.save(output_stream, format=image_format)
else:
raise ValueError('ERROR: invalid value for crop_type')
# TDE FIXME: should not have a ratio, makes no sense -> should have maximum width (std: 64; 256 px)
if thumbnail_ratio:
thumb_image = Image.open(StringIO.StringIO(output_stream.getvalue()))
thumb_image.thumbnail((new_w / thumbnail_ratio, new_h / thumbnail_ratio), Image.ANTIALIAS)
thumb_image.save(output_stream, image_format)
return output_stream.getvalue().encode('base64')
# ----------------------------------------
# Colors
# ---------------------------------------