def destinationsRect(self, destinations): """Return the rectangle containing all destinations.""" layout = self.view.surface().pageLayout() rect = QRect() for dest in destinations: for pageNum, r in dest: rect = rect.united(layout[pageNum].linkRect(r.normalized())) # not larger than viewport rect.setSize(rect.size().boundedTo(self.view.viewport().size())) return rect
def setGeometry(self, rect): QStackedLayout.setGeometry(self, rect) for i in range(self.count()): w = self.widget(i) hint = w.sizeHint() geom = QRect(rect) size = clipMinMax(rect.size(), w.minimumSize(), w.maximumSize()) size = fixSizePolicy(size, hint, w.sizePolicy()) geom.setSize(size) if geom != w.geometry(): w.setGeometry(geom)
def paintEvent(self, ev): image = self.viewer.image() painter = QPainter(self) if self.size() == image.size(): painter.drawImage(ev.rect(), image, ev.rect()) else: s = image.size() s.scale(self.size(), Qt.KeepAspectRatio) r = QRect() r.setSize(s) r.moveCenter(self.rect().center()) painter.drawPixmap(r, self.viewer.pixmap(s))
class Page(object): """Represents a page from a Poppler.Document. It maintains its own size and can draw itself using the cache. It also can maintain a list of links and return links at certain points or rectangles. The visible attribute (setVisible and visible) defaults to True but can be set to False to hide the page from a Surface (this is done by the Layout). """ def __init__(self, document, pageNumber): self._document = document self._pageNumber = pageNumber self._pageSize = document.page(pageNumber).pageSize() self._rotation = popplerqt4.Poppler.Page.Rotate0 self._rect = QRect() self._scale = 1.0 self._visible = True self._layout = lambda: None self._waiting = True # whether image still needs to be generated def document(self): """Returns the document.""" return self._document def pageNumber(self): """Returns the page number.""" return self._pageNumber def pageSize(self): """The page size in points (1/72 inch), taking rotation into account.""" return self._pageSize def layout(self): """Returns the Layout if we are part of one.""" return self._layout() def visible(self): """Returns True if this page is visible (will be displayed).""" return self._visible def setVisible(self, visible): """Sets whether this page is visible (will be displayed).""" self._visible = visible def rect(self): """Returns our QRect(), with position and size.""" return self._rect def size(self): """Returns our size.""" return self._rect.size() def height(self): """Returns our height.""" return self._rect.height() def width(self): """Returns our width.""" return self._rect.width() def pos(self): """Returns our position.""" return self._rect.topLeft() def setPos(self, point): """Sets our position (affects the Layout).""" self._rect.moveTopLeft(point) def setRotation(self, rotation): """Sets our Poppler.Page.Rotation.""" old, self._rotation = self._rotation, rotation if (old ^ rotation) & 1: self._pageSize.transpose() self.computeSize() def rotation(self): """Returns our rotation.""" return self._rotation def computeSize(self): """Recomputes our size.""" xdpi, ydpi = self.layout().dpi() if self.layout() else (72.0, 72.0) x = round(self._pageSize.width() * xdpi / 72.0 * self._scale) y = round(self._pageSize.height() * ydpi / 72.0 * self._scale) self._rect.setSize(QSize(x, y)) def setScale(self, scale): """Changes the display scale.""" self._scale = scale self.computeSize() def scale(self): """Returns our display scale.""" return self._scale def scaleForWidth(self, width): """Returns the scale we need to display ourselves at the given width.""" if self.layout(): return width * 72.0 / self.layout().dpi()[0] / self._pageSize.width() else: return float(width) / self._pageSize.width() def scaleForHeight(self, height): """Returns the scale we need to display ourselves at the given height.""" if self.layout(): return height * 72.0 / self.layout().dpi()[1] / self._pageSize.height() else: return float(height) / self._pageSize.height() def setWidth(self, width): """Change our scale to force our width to the given value.""" self.setScale(self.scaleForWidth(width)) def setHeight(self, height): """Change our scale to force our height to the given value.""" self.setScale(self.scaleForHeight(height)) def paint(self, painter, rect): update_rect = rect & self.rect() if not update_rect: return image_rect = QRect(update_rect.topLeft() - self.rect().topLeft(), update_rect.size()) image = cache.image(self) self._waiting = not image if image: painter.drawImage(update_rect, image, image_rect) else: # schedule an image to be generated, if done our update() method is called cache.generate(self) # find suitable image to be scaled from other size image = cache.image(self, False) if image: hscale = float(image.width()) / self.width() vscale = float(image.height()) / self.height() image_rect = QRectF(image_rect.x() * hscale, image_rect.y() * vscale, image_rect.width() * hscale, image_rect.height() * vscale) painter.drawImage(QRectF(update_rect), image, image_rect) else: # draw blank paper, using the background color of the cache rendering (if set) # or from the document itself. color = (cache.options(self.document()).paperColor() or cache.options().paperColor() or self.document().paperColor()) painter.fillRect(update_rect, color) def update(self): """Called when an image is drawn.""" # only redraw when we were waiting for a correctly sized image. if self._waiting and self.layout(): self.layout().updatePage(self) def repaint(self): """Call this to force a repaint (e.g. when the rendering options are changed).""" self._waiting = True cache.generate(self) def image(self, rect, xdpi=72.0, ydpi=None, options=None): """Returns a QImage of the specified rectangle (relative to our layout). xdpi defaults to 72.0 and ydpi defaults to xdpi. options may be a render.RenderOptions instance that will set some document rendering options just before rendering the image. """ rect = rect.normalized().intersected(self.rect()) if not rect: return rect.translate(-self.pos()) if ydpi is None: ydpi = xdpi hscale = (xdpi * self.pageSize().width()) / (72.0 * self.width()) vscale = (ydpi * self.pageSize().height()) / (72.0 * self.height()) x = rect.x() * hscale y = rect.y() * vscale w = rect.width() * hscale h = rect.height() * vscale with lock(self.document()): options and options.write(self.document()) page = self.document().page(self._pageNumber) image = page.renderToImage(xdpi, ydpi, x, y, w, h, self._rotation) image.setDotsPerMeterX(int(xdpi * 39.37)) image.setDotsPerMeterY(int(ydpi * 39.37)) return image def linksAt(self, point): """Returns a list() of zero or more links touched by point (relative to surface). The list is sorted with the smallest rectangle first. """ # Poppler.Link objects have their linkArea() ranging in width and height # from 0.0 to 1.0, so divide by resp. height and width of the Page. point = point - self.pos() x = float(point.x()) / self.width() y = float(point.y()) / self.height() # rotate if self._rotation: if self._rotation == popplerqt4.Poppler.Page.Rotate90: x, y = y, 1-x elif self._rotation == popplerqt4.Poppler.Page.Rotate180: x, y = 1-x, 1-y else: # 270 x, y = 1-y, x return list(sorted(cache.links(self).at(x, y), key=lambda link: link.linkArea().width())) def linksIn(self, rect): """Returns an unordered set() of links enclosed in rectangle (relative to surface).""" rect = rect.normalized() rect.translate(-self.pos()) left = float(rect.left()) / self.width() top = float(rect.top()) / self.height() right = float(rect.right()) / self.width() bottom = float(rect.bottom()) / self.height() # rotate if self._rotation: if self._rotation == popplerqt4.Poppler.Page.Rotate90: left, top, right, bottom = top, 1-right, bottom, 1-left elif self._rotation == popplerqt4.Poppler.Page.Rotate180: left, top, right, bottom = 1-right, 1-bottom, 1-left, 1-top else: # 270 left, top, right, bottom = 1-bottom, left, 1-top, right return cache.links(self).inside(left, top, right, bottom) def linkRect(self, linkarea): """Returns a QRect encompassing the linkArea (of a link) in coordinates of our rect().""" left, top, right, bottom = linkarea.normalized().getCoords() # rotate if self._rotation: if self._rotation == popplerqt4.Poppler.Page.Rotate90: left, top, right, bottom = 1-bottom, left, 1-top, right elif self._rotation == popplerqt4.Poppler.Page.Rotate180: left, top, right, bottom = 1-right, 1-bottom, 1-left, 1-top else: # 270 left, top, right, bottom = top, 1-right, bottom, 1-left rect = QRect() rect.setCoords(left * self.width(), top * self.height(), right * self.width(), bottom * self.height()) rect.translate(self.pos()) return rect def text(self, rect): """Returns text inside rectangle (relative to surface).""" rect = rect.normalized() rect.translate(-self.pos()) w, h = self.pageSize().width(), self.pageSize().height() left = float(rect.left()) / self.width() * w top = float(rect.top()) / self.height() * h right = float(rect.right()) / self.width() * w bottom = float(rect.bottom()) / self.height() * h if self._rotation: if self._rotation == popplerqt4.Poppler.Page.Rotate90: left, top, right, bottom = top, w-right, bottom, w-left elif self._rotation == popplerqt4.Poppler.Page.Rotate180: left, top, right, bottom = w-right, h-bottom, w-left, h-top else: # 270 left, top, right, bottom = h-bottom, left, h-top, right rect = QRectF() rect.setCoords(left, top, right, bottom) with lock(self.document()): page = self.document().page(self._pageNumber) return page.text(rect) def searchRect(self, rectF): """Returns a QRect encompassing the given rect (in points) to our position, size and rotation.""" rect = rectF.normalized() left, top, right, bottom = rect.getCoords() w, h = self.pageSize().width(), self.pageSize().height() hscale = self.width() / float(w) vscale = self.height() / float(h) if self._rotation: if self._rotation == popplerqt4.Poppler.Page.Rotate90: left, top, right, bottom = w-bottom, left, w-top, right elif self._rotation == popplerqt4.Poppler.Page.Rotate180: left, top, right, bottom = w-right, h-bottom, w-left, h-top else: # 270 left, top, right, bottom = top, h-right, bottom, h-left rect = QRect() rect.setCoords(left * hscale, top * vscale, right * hscale, bottom * vscale) return rect
class ImageAreaSelector (QtGui.QWidget): '''This widget provides means to visually crop a portion of an image by selecting it''' # pylint: disable=W0612 NAME = 'ImageAreaSelector' DESCRIPTION = 'A widget used to select part of an image' AUTHOR = 'Gabriele "Whisky" Visconti' WEBSITE = '' # pylint: enable=W0612 selection_changed = QtCore.pyqtSignal() def __init__(self, pixmap, parent=None): '''Constructor''' QtGui.QWidget.__init__(self, parent) self._pixmap = pixmap self._selection_rect = QRect() self._image_origin = QPoint() self._resize_start = None self._drag_start = None self._handle_size = QSize(-10, -10) self._painter = QtGui.QPainter() self._hl_color1 = QtGui.QPalette().color(QtGui.QPalette.Highlight) self._hl_color2 = QtGui.QPalette().color(QtGui.QPalette.Highlight) self._hl_color2.setAlpha(150) self._zoom = 1.0 self.adjust_minum_size() self.setBackgroundRole(QtGui.QPalette.Dark) self.setMouseTracking(True) self.setCursor(Qt.CrossCursor) # -------------------- [BEGIN] QT_OVERRIDE def mousePressEvent (self, event): '''Overrides QWidget's mousePressEvent. Handles starting a new selection, starting a drag operation''' # pylint: disable=C0103 mouse_pos = event.pos() / self._zoom sel_rect = self._selection_rect if not event.button() == Qt.LeftButton: return if (not sel_rect.isNull()) and sel_rect.contains(mouse_pos, True): handle_rect = QRect(sel_rect.bottomRight(), self._handle_size) if handle_rect.contains(mouse_pos): self._resize_start = mouse_pos else: self._drag_start = mouse_pos else: self._resize_start = mouse_pos sel_rect.setTopLeft (mouse_pos) self._selection_rect.setSize(QSize(0, 0)) def mouseMoveEvent (self, event): '''Overrides QWidget's mouseMoveEvent. Handles resizing and dragging operations on selection''' # pylint: disable=C0103 sel_rect = self._selection_rect if self._resize_start: resize_end = event.pos() / self._zoom sel_rect.setBottomRight(sel_rect.bottomRight() + (resize_end - self._resize_start)) self._resize_start = resize_end self.make_selection_square() self.update() elif self._drag_start is not None: drag_end = event.pos() / self._zoom sel_rect.translate(drag_end - self._drag_start) self._drag_start = drag_end self.update() # cursor shape: mouse_pos = event.pos() / self._zoom if (not sel_rect.isNull()) and sel_rect.contains(mouse_pos, True): handle_rect = QRect(sel_rect.bottomRight(), self._handle_size) if handle_rect.contains(mouse_pos): self.setCursor(Qt.SizeFDiagCursor) else: self.setCursor(Qt.OpenHandCursor) else: self.setCursor(Qt.CrossCursor) def mouseReleaseEvent (self, event): '''Overrides QWidget's mouseReleaseEvent. Handles ending a resizing or draggin operation on the selection''' # pylint: disable=C0103 self._selection_rect = self._selection_rect.normalized() self._resize_start = None self._drag_start = None self.update() if not self._selection_rect.isNull(): self.selection_changed.emit() def paintEvent(self, event): '''Overrides QWidtget's paintEvent.''' # pylint: disable=C0103 QtGui.QWidget.paintEvent(self, event) self._painter.begin(self) pixmap_dest_rect = QRect(self._image_origin * self._zoom, self._pixmap.size()*self._zoom) self._painter.drawPixmap(pixmap_dest_rect, self._pixmap) if not self._selection_rect.isNull(): # preparing the darkened frame: sel_rect = self._selection_rect.normalized() frame = QtGui.QPixmap(event.rect().size()) frame.fill(QtGui.QColor(0, 0, 0, 127)) frame_painter = QtGui.QPainter(frame) # erase the selected area from the frame: frame_painter.setCompositionMode( QtGui.QPainter.CompositionMode_DestinationIn) sel_rect_scaled = QRect(sel_rect.topLeft() * self._zoom, sel_rect.size() * self._zoom) frame_painter.fillRect(sel_rect_scaled, QtGui.QColor(0, 0, 0, 0)) # draw selection border : frame_painter.setCompositionMode( QtGui.QPainter.CompositionMode_SourceOver) frame_painter.setPen(self._hl_color1) frame_painter.drawRect(sel_rect_scaled) # draw the resize grip (if possible) if sel_rect_scaled.width() > 20 and sel_rect_scaled.height() > 20: handle_rect = QRect(sel_rect_scaled.bottomRight(), self._handle_size) frame_painter.fillRect(handle_rect, self._hl_color2) frame_painter.drawRect(handle_rect) frame_painter.end() # painting the darkened frame: self._painter.drawPixmap(0, 0, frame) self._painter.end() def resizeEvent(self, event): '''Overrides QWidget's resizeEvent. Handles image centering.''' # pylint: disable=C0103 self.adjust_image_origin() # -------------------- [END] QT_OVERRIDE def adjust_image_origin(self): '''Recalculates the top left corner's image position, so the image is painted centered''' # pylint: disable=C0103 new_size = self.size() / self._zoom pix_size = self._pixmap.size() dx = (new_size.width() - pix_size.width() ) /2 dy = (new_size.height() - pix_size.height()) /2 new_image_origin = QPoint(dx, dy) self._selection_rect.translate(new_image_origin - self._image_origin) self._image_origin = new_image_origin log.info('image origin: %s' % new_image_origin) def select_unscaled(self): '''Selects, if possible, a 96 x 96 square centered around the original image. In this way the image won't be scaled but won't take up all the 96 x 96 area.''' # pylint: disable=C0103 pix_size = self._pixmap.size() if pix_size.width() <= 96 and pix_size.height() <= 96: viewport_size = self.size() x = (viewport_size.width () - 96) / 2 y = (viewport_size.height() - 96) / 2 self._selection_rect.setTopLeft(QPoint(x, y)) self._selection_rect.setSize(QSize(96, 96)) self.update() self.selection_changed.emit() def select_all(self): '''Selects the whole image. Currently broken for images taller than wide.''' # TODO: make me work! self._selection_rect.setTopLeft(self._image_origin) self._selection_rect.setSize(self._pixmap.size()) self.update() self.selection_changed.emit() def rotate_left(self): '''Rotates the image counterclockwise.''' self._pixmap = self._pixmap.transformed(QtGui.QTransform().rotate(-90)) self.adjust_minum_size() self.adjust_image_origin() self.update() self.selection_changed.emit() def rotate_right(self): '''Rotates the image clockwise''' self._pixmap = self._pixmap.transformed(QtGui.QTransform().rotate(90)) self.adjust_minum_size() self.adjust_image_origin() self.update() self.selection_changed.emit() def adjust_minum_size(self): '''Sets the new minimum size, calculated upon the image size and the _zoom factor.''' pixmap = self._pixmap if pixmap.width() < 96 or pixmap.height() < 96: min_size = QSize(96, 96) else: min_size = pixmap.size() self.setMinimumSize(min_size*self._zoom) def make_selection_square(self): '''Modify the selected area making it square''' wid = self._selection_rect.width () self._selection_rect.setSize(QSize(wid, wid)) def set_zoom(self, zoomlevel): '''Sets the specified zoomlevel''' self._zoom = zoomlevel self.adjust_minum_size() self.adjust_image_origin() self.update() self.selection_changed.emit() def fit_zoom(self): '''Chooses a zoomlevel that makes visible the entire image. Currently broken.''' widget_wid = self.size().width () widget_hei = self.size().height() pixmap_wid = self._pixmap.width () pixmap_hei = self._pixmap.height() self._zoom = (min(widget_wid, widget_hei) / min(pixmap_wid, pixmap_hei)) self.adjust_minum_size() self.adjust_image_origin() self.update() self.selection_changed.emit() def get_selected_pixmap(self): '''Returns the pixmap contained in the selection rect. Currently doesn't handle transparency correctly''' sel_rect_scaled = QRect(self._selection_rect.topLeft() * self._zoom, self._selection_rect.size() * self._zoom) return QtGui.QPixmap.grabWidget(self, sel_rect_scaled)
class Page(object): """Represents a page from a Poppler.Document. It maintains its own size and can draw itself using the cache. It also can maintain a list of links and return links at certain points or rectangles. The visible attribute (setVisible and visible) defaults to True but can be set to False to hide the page from a Surface (this is done by the Layout). """ def __init__(self, document, pageNumber): self._document = document self._pageNumber = pageNumber self._pageSize = document.page(pageNumber).pageSize() self._rotation = popplerqt4.Poppler.Page.Rotate0 self._rect = QRect() self._scale = 1.0 self._visible = True self._layout = lambda: None self._waiting = True # whether image still needs to be generated def document(self): """Returns the document.""" return self._document def pageNumber(self): """Returns the page number.""" return self._pageNumber def pageSize(self): """The page size in points (1/72 inch), taking rotation into account.""" return self._pageSize def layout(self): """Returns the Layout if we are part of one.""" return self._layout() def visible(self): """Returns True if this page is visible (will be displayed).""" return self._visible def setVisible(self, visible): """Sets whether this page is visible (will be displayed).""" self._visible = visible def rect(self): """Returns our QRect(), with position and size.""" return self._rect def size(self): """Returns our size.""" return self._rect.size() def height(self): """Returns our height.""" return self._rect.height() def width(self): """Returns our width.""" return self._rect.width() def pos(self): """Returns our position.""" return self._rect.topLeft() def setPos(self, point): """Sets our position (affects the Layout).""" self._rect.moveTopLeft(point) def setRotation(self, rotation): """Sets our Poppler.Page.Rotation.""" old, self._rotation = self._rotation, rotation if (old ^ rotation) & 1: self._pageSize.transpose() self.computeSize() def rotation(self): """Returns our rotation.""" return self._rotation def computeSize(self): """Recomputes our size.""" xdpi, ydpi = self.layout().dpi() if self.layout() else (72.0, 72.0) x = round(self._pageSize.width() * xdpi / 72.0 * self._scale) y = round(self._pageSize.height() * ydpi / 72.0 * self._scale) self._rect.setSize(QSize(x, y)) def setScale(self, scale): """Changes the display scale.""" self._scale = scale self.computeSize() def scale(self): """Returns our display scale.""" return self._scale def scaleForWidth(self, width): """Returns the scale we need to display ourselves at the given width.""" if self.layout(): return width * 72.0 / self.layout().dpi( )[0] / self._pageSize.width() else: return float(width) / self._pageSize.width() def scaleForHeight(self, height): """Returns the scale we need to display ourselves at the given height.""" if self.layout(): return height * 72.0 / self.layout().dpi( )[1] / self._pageSize.height() else: return float(height) / self._pageSize.height() def setWidth(self, width): """Change our scale to force our width to the given value.""" self.setScale(self.scaleForWidth(width)) def setHeight(self, height): """Change our scale to force our height to the given value.""" self.setScale(self.scaleForHeight(height)) def image(self): """Render the page as an image or our size. Return a QImage.""" d = self._document w, h, r = self.width(), self.height(), self.rotation() page = d.page(self._pageNumber) pageSize = page.pageSize() if r & 1: pageSize.transpose() xres = 72.0 * w / pageSize.width() yres = 72.0 * h / pageSize.height() threshold = cache.options().oversampleThreshold() or cache.options( d).oversampleThreshold() multiplier = 2 if xres < threshold else 1 with lock(d): cache.options().write(d) cache.options(d).write(d) image = page.renderToImage(xres * multiplier, yres * multiplier, 0, 0, w * multiplier, h * multiplier, r) if multiplier == 2: image = image.scaledToWidth(w, Qt.SmoothTransformation) return image def paint(self, painter, rect): update_rect = rect & self.rect() if not update_rect: return image_rect = QRect(update_rect.topLeft() - self.rect().topLeft(), update_rect.size()) image = cache.image(self) self._waiting = not image if image: painter.drawImage(update_rect, image, image_rect) else: # schedule an image to be generated, if done our update() method is called cache.generate(self) # find suitable image to be scaled from other size image = cache.image(self, False) if image: hscale = float(image.width()) / self.width() vscale = float(image.height()) / self.height() image_rect = QRectF(image_rect.x() * hscale, image_rect.y() * vscale, image_rect.width() * hscale, image_rect.height() * vscale) painter.drawImage(QRectF(update_rect), image, image_rect) else: # draw blank paper, using the background color of the cache rendering (if set) # or from the document itself. color = (cache.options(self.document()).paperColor() or cache.options().paperColor() or self.document().paperColor()) painter.fillRect(update_rect, color) def update(self): """Called when an image is drawn.""" # only redraw when we were waiting for a correctly sized image. if self._waiting and self.layout(): self.layout().updatePage(self) def repaint(self): """Call this to force a repaint (e.g. when the rendering options are changed).""" self._waiting = True cache.generate(self) def image(self, rect, xdpi=72.0, ydpi=None, options=None): """Returns a QImage of the specified rectangle (relative to our layout). xdpi defaults to 72.0 and ydpi defaults to xdpi. options may be a render.RenderOptions instance that will set some document rendering options just before rendering the image. """ rect = rect.normalized().intersected(self.rect()) if not rect: return rect.translate(-self.pos()) if ydpi is None: ydpi = xdpi hscale = (xdpi * self.pageSize().width()) / (72.0 * self.width()) vscale = (ydpi * self.pageSize().height()) / (72.0 * self.height()) x = rect.x() * hscale y = rect.y() * vscale w = rect.width() * hscale h = rect.height() * vscale with lock(self.document()): options and options.write(self.document()) page = self.document().page(self._pageNumber) image = page.renderToImage(xdpi, ydpi, x, y, w, h, self._rotation) image.setDotsPerMeterX(int(xdpi * 39.37)) image.setDotsPerMeterY(int(ydpi * 39.37)) return image def linksAt(self, point): """Returns a list() of zero or more links touched by point (relative to surface). The list is sorted with the smallest rectangle first. """ # Poppler.Link objects have their linkArea() ranging in width and height # from 0.0 to 1.0, so divide by resp. height and width of the Page. point = point - self.pos() x = float(point.x()) / self.width() y = float(point.y()) / self.height() # rotate if self._rotation: if self._rotation == popplerqt4.Poppler.Page.Rotate90: x, y = y, 1 - x elif self._rotation == popplerqt4.Poppler.Page.Rotate180: x, y = 1 - x, 1 - y else: # 270 x, y = 1 - y, x return list( sorted(cache.links(self).at(x, y), key=lambda link: link.linkArea().width())) def linksIn(self, rect): """Returns an unordered set() of links enclosed in rectangle (relative to surface).""" rect = rect.normalized() rect.translate(-self.pos()) left = float(rect.left()) / self.width() top = float(rect.top()) / self.height() right = float(rect.right()) / self.width() bottom = float(rect.bottom()) / self.height() # rotate if self._rotation: if self._rotation == popplerqt4.Poppler.Page.Rotate90: left, top, right, bottom = top, 1 - right, bottom, 1 - left elif self._rotation == popplerqt4.Poppler.Page.Rotate180: left, top, right, bottom = 1 - right, 1 - bottom, 1 - left, 1 - top else: # 270 left, top, right, bottom = 1 - bottom, left, 1 - top, right return cache.links(self).inside(left, top, right, bottom) def linkRect(self, linkarea): """Returns a QRect encompassing the linkArea (of a link) in coordinates of our rect().""" left, top, right, bottom = linkarea.normalized().getCoords() # rotate if self._rotation: if self._rotation == popplerqt4.Poppler.Page.Rotate90: left, top, right, bottom = 1 - bottom, left, 1 - top, right elif self._rotation == popplerqt4.Poppler.Page.Rotate180: left, top, right, bottom = 1 - right, 1 - bottom, 1 - left, 1 - top else: # 270 left, top, right, bottom = top, 1 - right, bottom, 1 - left rect = QRect() rect.setCoords(left * self.width(), top * self.height(), right * self.width(), bottom * self.height()) rect.translate(self.pos()) return rect def text(self, rect): """Returns text inside rectangle (relative to surface).""" rect = rect.normalized() rect.translate(-self.pos()) w, h = self.pageSize().width(), self.pageSize().height() left = float(rect.left()) / self.width() * w top = float(rect.top()) / self.height() * h right = float(rect.right()) / self.width() * w bottom = float(rect.bottom()) / self.height() * h if self._rotation: if self._rotation == popplerqt4.Poppler.Page.Rotate90: left, top, right, bottom = top, w - right, bottom, w - left elif self._rotation == popplerqt4.Poppler.Page.Rotate180: left, top, right, bottom = w - right, h - bottom, w - left, h - top else: # 270 left, top, right, bottom = h - bottom, left, h - top, right rect = QRectF() rect.setCoords(left, top, right, bottom) with lock(self.document()): page = self.document().page(self._pageNumber) return page.text(rect) def searchRect(self, rectF): """Returns a QRect encompassing the given rect (in points) to our position, size and rotation.""" rect = rectF.normalized() left, top, right, bottom = rect.getCoords() w, h = self.pageSize().width(), self.pageSize().height() hscale = self.width() / float(w) vscale = self.height() / float(h) if self._rotation: if self._rotation == popplerqt4.Poppler.Page.Rotate90: left, top, right, bottom = w - bottom, left, w - top, right elif self._rotation == popplerqt4.Poppler.Page.Rotate180: left, top, right, bottom = w - right, h - bottom, w - left, h - top else: # 270 left, top, right, bottom = top, h - right, bottom, h - left rect = QRect() rect.setCoords(left * hscale, top * vscale, right * hscale, bottom * vscale) return rect
class ImageAreaSelector(QtGui.QWidget): '''This widget provides means to visually crop a portion of an image by selecting it''' # pylint: disable=W0612 NAME = 'ImageAreaSelector' DESCRIPTION = 'A widget used to select part of an image' AUTHOR = 'Gabriele "Whisky" Visconti' WEBSITE = '' # pylint: enable=W0612 selection_changed = QtCore.pyqtSignal() def __init__(self, pixmap, parent=None): '''Constructor''' QtGui.QWidget.__init__(self, parent) self._pixmap = pixmap self._selection_rect = QRect() self._image_origin = QPoint() self._resize_start = None self._drag_start = None self._handle_size = QSize(-10, -10) self._painter = QtGui.QPainter() self._hl_color1 = QtGui.QPalette().color(QtGui.QPalette.Highlight) self._hl_color2 = QtGui.QPalette().color(QtGui.QPalette.Highlight) self._hl_color2.setAlpha(150) self._zoom = 1.0 self.adjust_minum_size() self.setBackgroundRole(QtGui.QPalette.Dark) self.setMouseTracking(True) self.setCursor(Qt.CrossCursor) # -------------------- [BEGIN] QT_OVERRIDE def mousePressEvent(self, event): '''Overrides QWidget's mousePressEvent. Handles starting a new selection, starting a drag operation''' # pylint: disable=C0103 mouse_pos = event.pos() / self._zoom sel_rect = self._selection_rect if not event.button() == Qt.LeftButton: return if (not sel_rect.isNull()) and sel_rect.contains(mouse_pos, True): handle_rect = QRect(sel_rect.bottomRight(), self._handle_size) if handle_rect.contains(mouse_pos): self._resize_start = mouse_pos else: self._drag_start = mouse_pos else: self._resize_start = mouse_pos sel_rect.setTopLeft(mouse_pos) self._selection_rect.setSize(QSize(0, 0)) def mouseMoveEvent(self, event): '''Overrides QWidget's mouseMoveEvent. Handles resizing and dragging operations on selection''' # pylint: disable=C0103 sel_rect = self._selection_rect if self._resize_start: resize_end = event.pos() / self._zoom sel_rect.setBottomRight(sel_rect.bottomRight() + (resize_end - self._resize_start)) self._resize_start = resize_end self.make_selection_square() self.update() elif self._drag_start is not None: drag_end = event.pos() / self._zoom sel_rect.translate(drag_end - self._drag_start) self._drag_start = drag_end self.update() # cursor shape: mouse_pos = event.pos() / self._zoom if (not sel_rect.isNull()) and sel_rect.contains(mouse_pos, True): handle_rect = QRect(sel_rect.bottomRight(), self._handle_size) if handle_rect.contains(mouse_pos): self.setCursor(Qt.SizeFDiagCursor) else: self.setCursor(Qt.OpenHandCursor) else: self.setCursor(Qt.CrossCursor) def mouseReleaseEvent(self, event): '''Overrides QWidget's mouseReleaseEvent. Handles ending a resizing or draggin operation on the selection''' # pylint: disable=C0103 self._selection_rect = self._selection_rect.normalized() self._resize_start = None self._drag_start = None self.update() if not self._selection_rect.isNull(): self.selection_changed.emit() def paintEvent(self, event): '''Overrides QWidtget's paintEvent.''' # pylint: disable=C0103 QtGui.QWidget.paintEvent(self, event) self._painter.begin(self) pixmap_dest_rect = QRect(self._image_origin * self._zoom, self._pixmap.size() * self._zoom) self._painter.drawPixmap(pixmap_dest_rect, self._pixmap) if not self._selection_rect.isNull(): # preparing the darkened frame: sel_rect = self._selection_rect.normalized() frame = QtGui.QPixmap(event.rect().size()) frame.fill(QtGui.QColor(0, 0, 0, 127)) frame_painter = QtGui.QPainter(frame) # erase the selected area from the frame: frame_painter.setCompositionMode( QtGui.QPainter.CompositionMode_DestinationIn) sel_rect_scaled = QRect(sel_rect.topLeft() * self._zoom, sel_rect.size() * self._zoom) frame_painter.fillRect(sel_rect_scaled, QtGui.QColor(0, 0, 0, 0)) # draw selection border : frame_painter.setCompositionMode( QtGui.QPainter.CompositionMode_SourceOver) frame_painter.setPen(self._hl_color1) frame_painter.drawRect(sel_rect_scaled) # draw the resize grip (if possible) if sel_rect_scaled.width() > 20 and sel_rect_scaled.height() > 20: handle_rect = QRect(sel_rect_scaled.bottomRight(), self._handle_size) frame_painter.fillRect(handle_rect, self._hl_color2) frame_painter.drawRect(handle_rect) frame_painter.end() # painting the darkened frame: self._painter.drawPixmap(0, 0, frame) self._painter.end() def resizeEvent(self, event): '''Overrides QWidget's resizeEvent. Handles image centering.''' # pylint: disable=C0103 self.adjust_image_origin() # -------------------- [END] QT_OVERRIDE def adjust_image_origin(self): '''Recalculates the top left corner's image position, so the image is painted centered''' # pylint: disable=C0103 new_size = self.size() / self._zoom pix_size = self._pixmap.size() dx = (new_size.width() - pix_size.width()) / 2 dy = (new_size.height() - pix_size.height()) / 2 new_image_origin = QPoint(dx, dy) self._selection_rect.translate(new_image_origin - self._image_origin) self._image_origin = new_image_origin log.info('image origin: %s' % new_image_origin) def select_unscaled(self): '''Selects, if possible, a 96 x 96 square centered around the original image. In this way the image won't be scaled but won't take up all the 96 x 96 area.''' # pylint: disable=C0103 pix_size = self._pixmap.size() if pix_size.width() <= 96 and pix_size.height() <= 96: viewport_size = self.size() x = (viewport_size.width() - 96) / 2 y = (viewport_size.height() - 96) / 2 self._selection_rect.setTopLeft(QPoint(x, y)) self._selection_rect.setSize(QSize(96, 96)) self.update() self.selection_changed.emit() def select_all(self): '''Selects the whole image. Currently broken for images taller than wide.''' # TODO: make me work! self._selection_rect.setTopLeft(self._image_origin) self._selection_rect.setSize(self._pixmap.size()) self.update() self.selection_changed.emit() def rotate_left(self): '''Rotates the image counterclockwise.''' self._pixmap = self._pixmap.transformed(QtGui.QTransform().rotate(-90)) self.adjust_minum_size() self.adjust_image_origin() self.update() self.selection_changed.emit() def rotate_right(self): '''Rotates the image clockwise''' self._pixmap = self._pixmap.transformed(QtGui.QTransform().rotate(90)) self.adjust_minum_size() self.adjust_image_origin() self.update() self.selection_changed.emit() def adjust_minum_size(self): '''Sets the new minimum size, calculated upon the image size and the _zoom factor.''' pixmap = self._pixmap if pixmap.width() < 96 or pixmap.height() < 96: min_size = QSize(96, 96) else: min_size = pixmap.size() self.setMinimumSize(min_size * self._zoom) def make_selection_square(self): '''Modify the selected area making it square''' wid = self._selection_rect.width() self._selection_rect.setSize(QSize(wid, wid)) def set_zoom(self, zoomlevel): '''Sets the specified zoomlevel''' self._zoom = zoomlevel self.adjust_minum_size() self.adjust_image_origin() self.update() self.selection_changed.emit() def fit_zoom(self): '''Chooses a zoomlevel that makes visible the entire image. Currently broken.''' widget_wid = self.size().width() widget_hei = self.size().height() pixmap_wid = self._pixmap.width() pixmap_hei = self._pixmap.height() self._zoom = (min(widget_wid, widget_hei) / min(pixmap_wid, pixmap_hei)) self.adjust_minum_size() self.adjust_image_origin() self.update() self.selection_changed.emit() def get_selected_pixmap(self): '''Returns the pixmap contained in the selection rect. Currently doesn't handle transparency correctly''' sel_rect_scaled = QRect(self._selection_rect.topLeft() * self._zoom, self._selection_rect.size() * self._zoom) return QtGui.QPixmap.grabWidget(self, sel_rect_scaled)