# -*- coding: utf-8 -*-
#
# This file is part of the qpageview package.
#
# Copyright (c) 2019 - 2019 by Wilbert Berendsen
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# See http://www.gnu.org/licenses/ for more information.
"""
Export Pages to different file formats.
"""
import os
from PyQt5.QtCore import QBuffer, QIODevice, QMimeData, QPoint, QSizeF, Qt, QUrl
from PyQt5.QtGui import QDrag, QGuiApplication, QImage, QPageSize, QPdfWriter
from . import poppler
from . import util
[docs]class AbstractExporter:
"""Base class to export a rectangular area of a Page to a file.
Specialized subclasses implement each format.
You instantiate a subclass with a Page and a rectangle. The rectangle may
be None, to specify the full page. After instantiation, you can set
attributes to configure the export. The following attributes are supported::
resolution = 300
autocrop = False
oversample = 1
grayscale = False
paperColor = None
forceVector = True # force the render backend to be Arthur for
# exporting PDF pages to vector-based formats
After setting the attributes, you call one or more of save(), copyData(),
copyFile(), mimeData() or tempFileMimeData(), which will trigger the export
because they internally call data(), which caches its return value until
setPage() is called again.
Not all exporters support all attributes, the supportXXX attributes specify
whether an attribute is supported or not.
"""
# user settings:
resolution = 300
antialiasing = True
autocrop = False
oversample = 1
grayscale = False
paperColor = None
forceVector = True # force the render backend to be Arthur for PDF pages
# properties of exporter:
wantsVector = True
supportsResolution = True
supportsAntialiasing = True
supportsAutocrop = True
supportsOversample = True
supportsGrayscale = True
supportsPaperColor = True
mimeType = "application/octet-stream"
filename = ""
defaultBasename = "document"
defaultExt = ""
def __init__(self, page, rect=None):
self.setPage(page, rect)
[docs] def setPage(self, page, rect=None):
self._page = page.copy()
if self._page.renderer:
self._page.renderer = page.renderer.copy()
self._rect = rect
self._result = None # where the exported object is stored
self._tempFile = None
self._autoCropRect = None
self._document = None
self._pixmap = None
[docs] def page(self):
"""Return our page, setting the renderer to our preferences."""
p = self._page.copy()
p.paperColor = self.paperColor
if self._page.renderer:
p.renderer = self._page.renderer.copy()
p.renderer.paperColor = self.paperColor
p.renderer.antialiasing = self.antialiasing
if self.forceVector and self.wantsVector and \
isinstance(p, poppler.PopplerPage) and poppler.popplerqt5:
p.renderer.printRenderBackend = \
poppler.popplerqt5.Poppler.Document.ArthurBackend
return p
[docs] def autoCroppedRect(self):
"""Return the rect, autocropped if desired."""
if not self.autocrop:
return self._rect
if self._autoCropRect is None:
p = self._page
dpiX = p.width / p.defaultSize().width() * p.dpi
dpiY = p.height / p.defaultSize().height() * p.dpi
image = p.image(self._rect, dpiX, dpiY)
rect = util.autoCropRect(image)
# add one pixel to prevent loosing small joins or curves etc
rect = image.rect() & rect.adjusted(-1, -1, 1, 1)
if self._rect is not None:
rect.translate(self._rect.topLeft())
self._autoCropRect = rect
return self._autoCropRect
[docs] def export(self):
"""Perform the export, based on the settings, and return the exported data object."""
[docs] def successful(self):
"""Return True when export was successful."""
return self.data() is not None
[docs] def data(self):
"""Return the export result, assuming it is binary data of the exported file."""
if self._result is None:
self._result = self.export()
return self._result
[docs] def document(self):
"""Return a one-page Document to display the image to export.
Internally calls createDocument(), and caches the result, setting the
papercolor to the papercolor attribute if the exporter supports
papercolor.
"""
if self._document is None:
doc = self._document = self.createDocument()
if self.paperColor and self.paperColor.isValid():
for p in doc.pages():
p.paperColor = self.paperColor
return self._document
[docs] def createDocument(self):
"""Create and return a one-page Document to display the image to export."""
[docs] def renderer(self):
"""Return a renderer for the document(). By default, None is returned."""
return None
[docs] def copyData(self):
"""Copy the QMimeData() to the clipboard."""
QGuiApplication.clipboard().setMimeData(self.mimeData())
[docs] def mimeData(self):
"""Return a QMimeData() object representing the exported data."""
data = QMimeData()
data.setData(self.mimeType, self.data())
return data
[docs] def save(self, filename):
"""Save the exported image to a file."""
with open(filename, "wb") as f:
f.write(self.data())
[docs] def suggestedFilename(self):
"""Return a suggested file name for the file to export.
The name is based on the filename (if set) and also contains the
directory path. But the name will never be the same as the filename
set in the filename attribute.
"""
if self.filename:
base = os.path.splitext(self.filename)[0]
name = base + self.defaultExt
if name == self.filename:
name = base + "-export" + self.defaultExt
else:
name = self.defaultBasename + self.defaultExt
return name
[docs] def tempFilename(self):
"""Save data() to a tempfile and returns the filename."""
if self._tempFile is None:
if self.filename:
basename = os.path.splitext(os.path.basename(self.filename))[0]
else:
basename = self.defaultBasename
d = util.tempdir()
fname = self._tempFile = os.path.join(d, basename + self.defaultExt)
self.save(fname)
return self._tempFile
[docs] def tempFileMimeData(self):
"""Save the exported image to a temp file and return a QMimeData object for the url."""
data = QMimeData()
data.setUrls([QUrl.fromLocalFile(self.tempFilename())])
return data
[docs] def copyFile(self):
"""Save the exported image to a temp file and copy its name to the clipboard."""
QGuiApplication.clipboard().setMimeData(self.tempFileMimeData())
[docs] def pixmap(self, size=100):
"""Return a small pixmap to use for dragging etc."""
if self._pixmap is None:
paperColor = self.paperColor if self.supportsPaperColor else None
page = self.document().pages()[0]
self._pixmap = page.pixmap(paperColor=paperColor)
return self._pixmap
[docs] def drag(self, parent, mimeData):
"""Called by dragFile and dragData. Execs a QDrag on the mime data."""
d = QDrag(parent)
d.setMimeData(mimeData)
d.setPixmap(self.pixmap())
d.setHotSpot(QPoint(-10, -10))
return d.exec_(Qt.CopyAction)
[docs] def dragData(self, parent):
"""Start dragging the data. Parent can be any QObject."""
return self.drag(parent, self.mimeData())
[docs] def dragFile(self, parent):
"""Start dragging the data. Parent can be any QObject."""
return self.drag(parent, self.tempFileMimeData())
[docs]class ImageExporter(AbstractExporter):
"""Export a rectangular area of a Page (or the whole page) to an image."""
wantsVector = False
defaultBasename = "image"
defaultExt = ".png"
[docs] def export(self):
"""Create the QImage representing the exported image."""
res = self.resolution
if self.oversample != 1:
res *= self.oversample
i = self.page().image(self._rect, res, res, self.paperColor)
if self.oversample != 1:
i = i.scaled(i.size() / self.oversample, transformMode=Qt.SmoothTransformation)
if self.grayscale:
i = i.convertToFormat(QImage.Format_Grayscale8)
if self.autocrop:
i = i.copy(util.autoCropRect(i))
return i
[docs] def image(self):
return self.data()
[docs] def createDocument(self):
from . import image
return image.ImageDocument([self.image()], self.renderer())
[docs] def copyData(self):
QGuiApplication.clipboard().setImage(self.image())
[docs] def mimeData(self):
data = QMimeData()
data.setImageData(self.image())
return data
[docs] def save(self, filename):
if not self.image().save(filename):
raise OSError("Could not save image")
[docs]class SvgExporter(AbstractExporter):
"""Export a rectangular area of a Page (or the whole page) to a SVG file."""
mimeType = "image/svg"
supportsGrayscale = False
supportsOversample = False
defaultBasename = "image"
defaultExt = ".svg"
[docs] def export(self):
rect = self.autoCroppedRect()
buf = QBuffer()
buf.open(QBuffer.WriteOnly)
success = self.page().svg(buf, rect, self.resolution, self.paperColor)
buf.close()
if success:
return buf.data()
[docs] def createDocument(self):
from . import svg
return svg.SvgDocument([self.data()], self.renderer())
[docs]class PdfExporter(AbstractExporter):
"""Export a rectangular area of a Page (or the whole page) to a PDF file."""
mimeType = "application/pdf"
supportsGrayscale = False
supportsOversample = False
defaultExt = ".pdf"
[docs] def export(self):
rect = self.autoCroppedRect()
buf = QBuffer()
buf.open(QBuffer.WriteOnly)
success = self.page().pdf(buf, rect, self.resolution, self.paperColor)
buf.close()
if success:
return buf.data()
[docs] def createDocument(self):
from . import poppler
return poppler.PopplerDocument(self.data(), self.renderer())
[docs]class EpsExporter(AbstractExporter):
"""Export a rectangular area of a Page (or the whole page) to an EPS file."""
mimeType = "application/postscript"
supportsGrayscale = False
supportsOversample = False
defaultExt = ".eps"
[docs] def export(self):
rect = self.autoCroppedRect()
buf = QBuffer()
buf.open(QBuffer.WriteOnly)
success = self.page().eps(buf, rect, self.resolution, self.paperColor)
buf.close()
if success:
return buf.data()
[docs] def createDocument(self):
from . import poppler
rect = self.autoCroppedRect()
buf = QBuffer()
buf.open(QBuffer.WriteOnly)
success = self.page().pdf(buf, rect, self.resolution, self.paperColor)
buf.close()
return poppler.PopplerDocument(buf.data(), self.renderer())
[docs]def pdf(filename, pageList, resolution=72, paperColor=None):
"""Export the pages in pageList to a PDF document.
filename can be a string or any QIODevice. The pageList is a list of the
Page objects to export.
Normally vector graphics are rendered, but in cases where that is not
possible, the resolution will be used to determine the DPI for the
generated rendering.
The computedRotation attribute of the pages is used to determine the
rotation.
Make copies of the pages if you run this function in a background thread.
"""
pdf = QPdfWriter(filename)
pdf.setCreator("qpageview")
pdf.setResolution(resolution)
for n, page in enumerate(pageList):
# map to the original page
source = page.pageRect()
# scale to target size
w = source.width() * page.scaleX
h = source.height() * page.scaleY
if page.computedRotation & 1:
w, h = h, w
targetSize = QSizeF(w, h)
if n:
pdf.newPage()
layout = pdf.pageLayout()
layout.setMode(layout.FullPageMode)
layout.setPageSize(QPageSize(targetSize * 72.0 / page.dpi, QPageSize.Point))
pdf.setPageLayout(layout)
# TODO handle errors?
page.output(pdf, source, paperColor)