So I am attempting to make a GTK application using python, and I have run into this issue where after I place an image on a window, I can increase the size of the window, but not decrease it. Given that the purpose of this particular window is to display a resizable image, this is rather bothersome.
I have extracted the relevant code which demonstrates this behavior below
#!/usr/bin/env python
from gi.repository import Gtk, GdkPixbuf
import sys
class ImageWindow(Gtk.Window):
def __init__(self, image_data):
Gtk.Window.__init__(self, title="image test")
if image_data and len(image_data) > 0:
self.loader = GdkPixbuf.PixbufLoader()
self.loader.write(image_data)
self.pixbuf = self.loader.get_pixbuf()
self.image = Gtk.Image.new_from_pixbuf(self.pixbuf)
else:
self.image = Gtk.Image.new()
self.add(self.image)
self.connect('delete-event', Gtk.main_quit)
win = ImageWindow(sys.stdin.read())
win.show_all()
Gtk.main()
If you pipe in nothing, the window resizes fine. Pipe in an image, and the form clicks to the size of the image, and can resize bigger, but cannot resize smaller.
So here is an example of a scaling image. The idea is that you put the image in a Gtk.ScrolledWindow() and resize the image as soon as the window is resized.:
#!/usr/bin/env python
from gi.repository import Gtk, GdkPixbuf, GLib
import sys
class ImageWindow(Gtk.Window):
def __init__(self, image_data):
Gtk.Window.__init__(self, title="image test")
self.connect('delete-event', Gtk.main_quit)
self.image = Gtk.Image()
scrolled_window = Gtk.ScrolledWindow()
scrolled_window.add(self.image)
self.add(scrolled_window)
if len(image_data) == 0:
return
self.loader = GdkPixbuf.PixbufLoader()
self.loader.write(image_data)
self.loader.close()
self.pixbuf = self.loader.get_pixbuf()
self.image.set_from_pixbuf(self.pixbuf)
width = self.pixbuf.get_width()
height = self.pixbuf.get_height()
self.dimension = float(width) / height
self.set_default_size(width, height)
self.connect('check-resize', self.on_resize)
def on_resize(self, window):
width, height = self.get_size()
if float(width) / height > self.dimension:
self.pixbuf = self.pixbuf.scale_simple(
self.dimension * height,
height,
GdkPixbuf.InterpType.NEAREST
)
else:
self.pixbuf = self.pixbuf.scale_simple(
width,
width / self.dimension,
GdkPixbuf.InterpType.NEAREST
)
GLib.idle_add(self.image.set_from_pixbuf, self.pixbuf)
win = ImageWindow(sys.stdin.read())
win.show_all()
Gtk.main()
As an alternative, you can load the pixbuf again from the loader and scale it afterwards. This looks better if you make your image smaller and then larger again but needs more processing:
def on_resize(self, window):
width, height = self.get_size()
self.pixbuf = self.loader.get_pixbuf()
if float(width) / height > self.dimension:
self.pixbuf = self.pixbuf.scale_simple(
self.dimension * height,
height,
GdkPixbuf.InterpType.BILINEAR
)
else:
self.pixbuf = self.pixbuf.scale_simple(
width,
width / self.dimension,
GdkPixbuf.InterpType.BILINEAR
)
GLib.idle_add(self.image.set_from_pixbuf, self.pixbuf)
Related
I am new to Python GUI development with Tkinter.
I am trying to make the app window a certain size and I want the webcam view in the window to fill the size of the main window.
When I set the window size, the webcam does not fill the entire window.
How can I make the elements fill the entire window when I customize the size of the window?
Here is my code:
import tkinter as tk
from PIL import Image, ImageTk
import cv2
class MainWindow():
def __init__(self, window, cap):
self.window = window
self.cap = cap
self.width = self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)
self.height = self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
self.interval = 20 # Interval in ms to get the latest frame
# Create canvas for image
self.canvas = tk.Canvas(self.window, width=self.width, height=self.height)
self.canvas.grid(row=0, column=0)
self.canvas.pack(fill="both", expand=True)
# Update image on canvas
self.update_image()
def update_image(self):
# Get the latest frame and convert image format
self.image = cv2.cvtColor(self.cap.read()[1], cv2.COLOR_BGR2RGB) # to RGB
self.image = Image.fromarray(self.image) # to PIL format
self.image = ImageTk.PhotoImage(self.image) # to ImageTk format
# Update image
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.image)
# Repeat every 'interval' ms
self.window.after(self.interval, self.update_image)
if __name__ == "__main__":
root = tk.Tk()
root.geometry('600x800')
# root.resizable(width=0, height=0)
MainWindow(root, cv2.VideoCapture(0))
root.mainloop()
Thanks.
All you have to do is resize the image to fit the canvas size. You can either use cv2.resize() or Image.resize() to resize the image.
To get the current canvas size use canvas.winfo_height() and canvas.wifo_width(). Besides that, you should also consider updating the existing image using canvas.itemconfig(tag_id), instead of creating a new one each time.
sample code (I'll be using a label instead of canvas to display the image):
import tkinter as tk
from PIL import Image, ImageTk
import cv2
class MainWindow():
def __init__(self, window, cap):
self.window = window
self.cap = cap
self.interval = 20 # Interval in ms to get the latest frame
# Create canvas for image
self.vid_lbl = tk.Label(self.window)
self.vid_lbl.pack(fill="both", expand=True)
# Update image on canvas
self.update_image()
def update_image(self):
# Get the latest frame and convert image format
width, height = self.vid_lbl.winfo_width(), self.vid_lbl.winfo_height()
self.image = cv2.cvtColor(self.cap.read()[1], cv2.COLOR_BGR2RGB) # to RGB
self.image = cv2.resize(self.image, (width, height), cv2.INTER_AREA)
self.image = Image.fromarray(self.image) # to PIL format
#self.image = self.image.resize((width, height))
self.image = ImageTk.PhotoImage(self.image) # to ImageTk format
self.vid_lbl.config(image=self.image)
# Repeat every 'interval' ms
self.window.after(self.interval, self.update_image)
if __name__ == "__main__":
root = tk.Tk()
root.geometry('600x800')
# root.resizable(width=0, height=0)
MainWindow(root, cv2.VideoCapture(0))
root.mainloop()
I have a PyQt application where I have drawn points using QPainter over a QGraphicsScene and made a drag n drop sort of a thing.
Now, there is one issue which I'm facing and that is I'm unable to drag those points at the extreme corner and edges of QGraphicsScene. It always seems as if some amount of padding or space is left.
How do I get round this problem?
Code:
from collections import deque
from datetime import datetime
import sys
from threading import Thread
import time
import numpy as np
import cv2
from PyQt4 import QtCore, QtGui
class CameraWidget(QtGui.QGraphicsView):
"""Independent camera feed
Uses threading to grab IP camera frames in the background
#param width - Width of the video frame
#param height - Height of the video frame
#param stream_link - IP/RTSP/Webcam link
#param aspect_ratio - Whether to maintain frame aspect ratio or force into fraame
"""
def __init__(self, width, height, stream_link=0, aspect_ratio=False, parent=None, deque_size=1):
super(CameraWidget, self).__init__(parent)
# Initialize deque used to store frames read from the stream
self.deque = deque(maxlen=deque_size)
self.screen_width = width
self.screen_height = height
self.maintain_aspect_ratio = aspect_ratio
self.camera_stream_link = stream_link
# Flag to check if camera is valid/working
self.online = False
self.capture = None
self.setScene(QtGui.QGraphicsScene(self))
self._pixmap_item = self.scene().addPixmap(QtGui.QPixmap())
canvas = Canvas()
lay = QtGui.QVBoxLayout()
lay.addWidget(canvas)
self.setLayout(lay)
self.load_network_stream()
# Start background frame grabbing
self.get_frame_thread = Thread(target=self.get_frame, args=())
self.get_frame_thread.daemon = True
self.get_frame_thread.start()
# Periodically set video frame to display
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.set_frame)
self.timer.start(0.5)
print("Started camera: {}".format(self.camera_stream_link))
def load_network_stream(self):
"""Verifies stream link and open new stream if valid"""
def load_network_stream_thread():
if self.verify_network_stream(self.camera_stream_link):
self.capture = cv2.VideoCapture(self.camera_stream_link)
self.online = True
self.load_stream_thread = Thread(target=load_network_stream_thread, args=())
self.load_stream_thread.daemon = True
self.load_stream_thread.start()
def verify_network_stream(self, link):
"""Attempts to receive a frame from given link"""
cap = cv2.VideoCapture(link)
if not cap.isOpened():
return False
cap.release()
return True
def get_frame(self):
"""Reads frame, resizes, and converts image to pixmap"""
while True:
try:
if self.capture.isOpened() and self.online:
# Read next frame from stream and insert into deque
status, frame = self.capture.read()
if status:
self.deque.append(frame)
else:
self.capture.release()
self.online = False
else:
# Attempt to reconnect
print("attempting to reconnect", self.camera_stream_link)
self.load_network_stream()
self.spin(2)
self.spin(0.001)
except AttributeError:
pass
def spin(self, seconds):
"""Pause for set amount of seconds, replaces time.sleep so program doesnt stall"""
time_end = time.time() + seconds
while time.time() < time_end:
QtGui.QApplication.processEvents()
def set_frame(self):
"""Sets pixmap image to video frame"""
if not self.online:
self.spin(1)
return
if self.deque and self.online:
# Grab latest frame
frame = self.deque[-1]
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = frame.shape
bytesPerLine = ch * w
# Convert to pixmap and set to video frame
image = QtGui.QImage(frame, w, h, bytesPerLine, QtGui.QImage.Format_RGB888)
pixmap = QtGui.QPixmap.fromImage(image.copy())
self._pixmap_item.setPixmap(pixmap)
self.fix_size()
def resizeEvent(self, event):
self.fix_size()
super().resizeEvent(event)
def fix_size(self):
self.fitInView(
self._pixmap_item,
QtCore.Qt.KeepAspectRatio
if self.maintain_aspect_ratio
else QtCore.Qt.IgnoreAspectRatio,
)
class Window(QtGui.QWidget):
def __init__(self, cam=None, parent=None):
super(Window, self).__init__(parent)
self.showMaximized()
self.screen_width = self.width()
self.screen_height = self.height()
# Create camera widget
print("Creating Camera Widget...")
self.camera = CameraWidget(self.screen_width, self.screen_height, cam)
lay = QtGui.QVBoxLayout(self)
lay.setContentsMargins(0, 0, 0, 0)
lay.setSpacing(0)
lay.addWidget(self.camera)
class Canvas(QtGui.QWidget):
DELTA = 200 #for the minimum distance
def __init__(self, parent=None):
super(Canvas, self).__init__(parent)
self.draggin_idx = -1
self.points = np.array([[x[0],x[1]] for x in [[100,200], [200,200], [100,400], [200,400]]], dtype=np.float)
self.id = None
self.points_dict = {}
for i, x in enumerate(self.points):
point=(int(x[0]),int(x[1]))
self.points_dict[i] = point
def paintEvent(self, e):
qp = QtGui.QPainter()
qp.begin(self)
self.drawPoints(qp)
self.drawLines(qp)
qp.end()
def drawPoints(self, qp):
pen = QtGui.QPen()
pen.setWidth(10)
pen.setColor(QtGui.QColor('red'))
qp.setPen(pen)
for x,y in self.points:
qp.drawPoint(x,y)
def drawLines(self, qp):
qp.setPen(QtCore.Qt.red)
qp.drawLine(self.points_dict[0][0], self.points_dict[0][1], self.points_dict[1][0], self.points_dict[1][1])
qp.drawLine(self.points_dict[1][0], self.points_dict[1][1], self.points_dict[3][0], self.points_dict[3][1])
qp.drawLine(self.points_dict[3][0], self.points_dict[3][1], self.points_dict[2][0], self.points_dict[2][1])
qp.drawLine(self.points_dict[2][0], self.points_dict[2][1], self.points_dict[0][0], self.points_dict[0][1])
def _get_point(self, evt):
pos = evt.pos()
if pos.x() < 0:
pos.setX(0)
elif pos.x() > self.width():
pos.setX(self.width())
if pos.y() < 0:
pos.setY(0)
elif pos.y() > self.height():
pos.setY(self.height())
return np.array([pos.x(), pos.y()])
#get the click coordinates
def mousePressEvent(self, evt):
if evt.button() == QtCore.Qt.LeftButton and self.draggin_idx == -1:
point = self._get_point(evt)
int_point = (int(point[0]), int(point[1]))
min_dist = ((int_point[0]-self.points_dict[0][0])**2 + (int_point[1]-self.points_dict[0][1])**2)**0.5
for i, x in enumerate(list(self.points_dict.values())):
distance = ((int_point[0]-x[0])**2 + (int_point[1]-x[1])**2)**0.5
if min_dist >= distance:
min_dist = distance
self.id = i
#dist will hold the square distance from the click to the points
dist = self.points - point
dist = dist[:,0]**2 + dist[:,1]**2
dist[dist>self.DELTA] = np.inf #obviate the distances above DELTA
if dist.min() < np.inf:
self.draggin_idx = dist.argmin()
def mouseMoveEvent(self, evt):
if self.draggin_idx != -1:
point = self._get_point(evt)
self.points[self.draggin_idx] = point
self.update()
def mouseReleaseEvent(self, evt):
if evt.button() == QtCore.Qt.LeftButton and self.draggin_idx != -1:
point = self._get_point(evt)
int_point = (int(point[0]), int(point[1]))
self.points_dict[self.id] = int_point
self.points[self.draggin_idx] = point
self.draggin_idx = -1
self.update()
camera = 0
if __name__ == "__main__":
app = QtGui.QApplication([])
win = Window(camera)
sys.exit(app.exec_())
Edit:
I've one more requirement.
The mousePressEvent and mouseReleaseEvent in my Canvas class gives me coordinates w.r.t. my monitor resolution, instead I want it w.r.t. QGraphicsView. Say e.g. my screen_resolution is 1920x1080 and the size of my QGraphicsView is 640x480 then I should get points in accordance with 640x480.
The simplest solution would be to add lay.setContentsMargins(0, 0, 0, 0) for the layout of the graphics view:
class CameraWidget(QtGui.QGraphicsView):
def __init__(self, width, height, stream_link=0, aspect_ratio=False, parent=None, deque_size=1):
# ...
canvas = Canvas()
lay = QtGui.QVBoxLayout()
lay.addWidget(canvas)
self.setLayout(lay)
lay.setContentsMargins(0, 0, 0, 0)
# ...
But consider that doing all this is not suggested.
First of all, you don't need a layout for a single widget, as you could just create the widget with the view as a parent and then resize it in the resizeEvent:
# ...
self.canvas = Canvas(self)
def resizeEvent(self, event):
self.fix_size()
super().resizeEvent(event)
self.canvas.resize(self.size())
Widgets like QGraphicsView should not have a layout set, it's unsupported and may lead to unwanted behavior or even bugs under certain conditions.
In any case, it doesn't make a lot of sense to add a widget on top of a QGraphicsView if that widget is used for painting and mouse interaction: QGraphicsView already provides better implementation for that by using QGraphicsRectItem or QGraphicsLineItem.
And, even if it weren't the case, custom drawing over a graphics view should be done in its drawForeground() implementation.
(Python 2.7). I have a Tkinter canvas with two images that are both the height and width of the canvas, so they cover the whole window. One image is on top of the other. I want to, using the mouse, be able to erase part of the top image wherever I want, thus exposing the bottom image. Is this possible?
I'm curious in how to implement the Home.erase method below which is bound to a Tkinter motion event.
# -*- coding: utf-8 -*-
import io
from PIL import Image, ImageTk
import Tkinter as tk
#Image 2 is on top of image 1.
IMAGE1_DIR = "C:/path_to_image/image1.png"
IMAGE2_DIR = "C:/path_to_image/image2.png"
def create_image(filename, width=0, height=0):
"""
Returns a PIL.Image object from filename - sized
according to width and height parameters.
filename: str.
width: int, desired image width.
height: int, desired image height.
1) If neither width nor height is given, image will be returned as is.
2) If both width and height are given, image will resized accordingly.
3) If only width or only height is given, image will be scaled so specified
parameter is satisfied while keeping image's original aspect ratio the same.
"""
with open(filename, "rb") as f:
fh = io.BytesIO(f.read())
#Create a PIL image from the data
img = Image.open(fh, mode="r")
#Resize if necessary.
if not width and not height:
return img
elif width and height:
return img.resize((int(width), int(height)), Image.ANTIALIAS)
else: #Keep aspect ratio.
w, h = img.size
scale = width/float(w) if width else height/float(h)
return img.resize((int(w*scale), int(h*scale)), Image.ANTIALIAS)
class Home(object):
"""
master: tk.Tk window.
screen: tuple, (width, height).
"""
def __init__(self, master, screen):
self.screen = w, h = screen
self.master = master
self.frame = tk.Frame(self.master)
self.frame.pack()
self.can = tk.Canvas(self.frame, width=w, height=h)
self.can.pack()
#Photos will be as large as the screen.
p1 = ImageTk.PhotoImage(image=create_image(IMAGE1_DIR, w, h))
p2 = ImageTk.PhotoImage(image=create_image(IMAGE2_DIR, w, h))
## Place photos in center of screen.
## Create label to retain a reference to image so it doesn't dissapear.
self.photo1 = self.can.create_image((w//2, h//2), image=p1)
label1 = tk.Label(image=p1)
label1.image = p1
#On top.
self.photo2 = self.can.create_image((w//2, h//2), image=p2)
label2 = tk.Label(image=p2)
label2.image = p2
#Key bindings.
self.master.bind("<Return>", self.reset)
self.master.bind("<Motion>", self.erase)
#### Key Bindings ####
def reset(self, event):
""" Enter/Return key. """
self.frame.destroy()
self.__init__(self.master, self.screen)
def erase(self, event):
"""
Mouse motion binding.
Erase part of top image (self.photo2) at location (event.x, event.y),
consequently exposing part of the bottom image (self.photo1).
"""
pass
def main(screen=(500, 500)):
root = tk.Tk()
root.resizable(0, 0)
Home(root, screen)
#Place window in center of screen.
root.eval('tk::PlaceWindow %s center'%root.winfo_pathname(root.winfo_id()))
root.mainloop()
if __name__ == '__main__':
main()
Thanks to #martineau for the suggestions! Here is the working code.
from PIL import Image, ImageTk
import Tkinter as tk
#Image 2 is on top of image 1.
IMAGE1_DIR = "C:/path/image1.PNG"
IMAGE2_DIR = "C:/path/image2.PNG"
#Brush size in pixels.
BRUSH = 5
#Actual size is 2*BRUSH
def create_image(filename, width=0, height=0):
"""
Returns a PIL.Image object from filename - sized
according to width and height parameters.
filename: str.
width: int, desired image width.
height: int, desired image height.
1) If neither width nor height is given, image will be returned as is.
2) If both width and height are given, image will resized accordingly.
3) If only width or only height is given, image will be scaled so specified
parameter is satisfied while keeping image's original aspect ratio the same.
"""
#Create a PIL image from the file.
img = Image.open(filename, mode="r")
#Resize if necessary.
if not width and not height:
return img
elif width and height:
return img.resize((int(width), int(height)), Image.ANTIALIAS)
else: #Keep aspect ratio.
w, h = img.size
scale = width/float(w) if width else height/float(h)
return img.resize((int(w*scale), int(h*scale)), Image.ANTIALIAS)
class Home(object):
"""
master: tk.Tk window.
screen: tuple, (width, height).
"""
def __init__(self, master, screen):
self.screen = w, h = screen
self.master = master
self.frame = tk.Frame(self.master)
self.frame.pack()
self.can = tk.Canvas(self.frame, width=w, height=h)
self.can.pack()
self.image1 = create_image(IMAGE1_DIR, w, h)
self.image2 = create_image(IMAGE2_DIR, w, h)
#Center of screen.
self.center = w//2, h//2
#Start with no photo on the screen.
self.photo = False
#Draw photo on screen.
self.draw()
#Key bindings.
self.master.bind("<Return>", self.reset)
self.master.bind("<Motion>", self.erase)
def draw(self):
"""
If there is a photo on the canvas, destroy it.
Draw self.image2 on the canvas.
"""
if self.photo:
self.can.delete(self.photo)
self.label.destroy()
p = ImageTk.PhotoImage(image=self.image2)
self.photo = self.can.create_image(self.center, image=p)
self.label = tk.Label(image=p)
self.label.image = p
#### Key Bindings ####
def reset(self, event):
""" Enter/Return key. """
self.frame.destroy()
self.__init__(self.master, self.screen)
def erase(self, event):
"""
Mouse motion binding.
Erase part of top image (self.photo2) at location (event.x, event.y),
consequently exposing part of the bottom image (self.photo1).
"""
for x in xrange(event.x-BRUSH, event.x+BRUSH+1):
for y in xrange(event.y-BRUSH, event.y+BRUSH+1):
try:
p = self.image1.getpixel((x, y))
self.image2.putpixel((x, y), p)
except IndexError:
pass
self.draw()
def main(screen=(500, 500)):
root = tk.Tk()
root.resizable(0, 0)
Home(root, screen)
#Place window in center of screen.
root.eval('tk::PlaceWindow %s center'%root.winfo_pathname(root.winfo_id()))
root.mainloop()
if __name__ == '__main__':
main()
I have created a short script to display a photo in a Tkinter canvas element. I size the canvas element to have the same size as the photo, but when I launch the window, I generally only see a small portion of the image, no matter how much I expand the window.
Also when I print out the size of the image, which I am using to set the size of the canvas, this size says (922, 614) whereas when I record mouse clicks on the canvas, the lower-right hand corner is at something like (500, 300). My code is below. What should I change so that the canvas is the same size as the image and fully shows the image?
class AppWindow(Frame):
def __init__(self, parent, list_of_files, write_file):
Frame.__init__(self, parent)
self.parent = parent
...
self.image = None
self.canvas = None
self.index = -1
self.loadImage()
self.initUI()
self.resetCanvas()
def initUI(self):
self.style = Style()
self.style.theme_use("default")
self.pack(fill=BOTH, expand=1)
...
self.canvas = Tkinter.Canvas(self, width = self.image.width(), height = self.image.height())
def loadImage(self):
self.index += 1
img = cv2.imread(self.list_of_files[self.index])
img = cv2.resize(img, (0,0), fx = IMAGE_RESIZE_FACTOR, fy = IMAGE_RESIZE_FACTOR)
b, g, r = cv2.split(img)
img = cv2.merge((r,g,b))
im = Image.fromarray(img)
self.image = ImageTk.PhotoImage(image=im)
def resetCanvas(self):
self.canvas.create_image(0, 0, image=self.image)
self.canvas.place(x = 0, y = 0, height = self.image.height(), width = self.image.width())
Here is a screenshot showing a photo and how it is presented in the Tkinter canvas:
Here's what I have tried so far:
Not resizing the image or changing the resizing amount - this doesn't do anything
Making the canvas double the size of the image rather than equal to the image, likes so self.canvas = Tkinter.Canvas(self, width = self.image.width()*2, height = self.image.height()*2) This does make the canvas larger (I can tell because I have some drawing functions on the canvas), but the image stays the same small size, not showing the entire image
When I display the cv2 format image with cv2.imshow() I see the full image, so it's not cv2 that is cutting off portions of the image.
I realized what I have above is not a complete working example. I've pared the script down, and now running this I still see the same problem:
import Tkinter
import Image, ImageTk
from Tkinter import Tk, BOTH
from ttk import Frame, Button, Style
import cv2
import os
import time
import itertools
IMAGE_RESIZE_FACTOR = .3
class AppWindow(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.loadImage()
self.initUI()
def loadImage(self):
img = cv2.imread("w_4131.jpg")
img = cv2.resize(img, (0,0), fx = IMAGE_RESIZE_FACTOR, fy = IMAGE_RESIZE_FACTOR)
b, g, r = cv2.split(img)
img = cv2.merge((r,g,b))
im = Image.fromarray(img)
self.image = ImageTk.PhotoImage(image=im)
def initUI(self):
self.style = Style()
self.style.theme_use("default")
self.pack(fill=BOTH, expand=1)
print "width and height of image should be ", self.image.width(), self.image.height()
self.canvas = Tkinter.Canvas(self, width = self.image.width(), height = self.image.height())
self.canvas.pack()
self.canvas.create_image(0, 0, image=self.image)
def main():
root = Tk()
root.geometry("250x150+300+300")
app = AppWindow(root)
root.mainloop()
if __name__ == '__main__':
main()
When you place an image on a canvas at 0,0, that specifies the location of the center of the image, not the upper-left corner. The image is all there, it's just that you're only seeing the bottom-right corner.
Add anchor="nw" when creating the image for the coordinates to represent the upper-left corner of the image:
self.canvas.create_image(0, 0, image=self.image, anchor="nw")
As for the canvas not being the same size of the image, I'm not seeing that. The image seems to fit the canvas perfectly.
I want to save textview's buffer in pdf format. I can do it using reportlab if it's just a simple text. But, what if I want to save everything, from text with its tags and also images?
from gi.repository import Gtk, GdkPixbuf
import pango
from reportlab.pdfgen import canvas
class gui():
def __init__(self):
self.window = Gtk.Window()
self.window.connect('delete-event', Gtk.main_quit)
self.box = Gtk.Box()
self.window.add(self.box)
self.textview = Gtk.TextView()
self.textbuffer = self.textview.get_buffer()
self.tag_bold = self.textbuffer.create_tag("bold",
weight=pango.WEIGHT_BOLD)
self.tag_italic = self.textbuffer.create_tag("italic",
style=pango.STYLE_ITALIC)
pix = GdkPixbuf.Pixbuf.new_from_file_at_size('baby.jpg', 50, 50)
tag = [self.tag_bold,
self.tag_italic]
self.textbuffer.insert_pixbuf(self.textbuffer.get_end_iter(), pix)
for i in range(20):
self.textbuffer.insert_with_tags(self.textbuffer.get_end_iter(),
'line%d\n' % (i+1),
tag[i % 2])
self.box.pack_start(self.textview, True, True, 0)
self.button = Gtk.Button(label='Start')
self.button.connect('clicked', self.on_button_clicked)
self.box.pack_start(self.button, True, True, 0)
self.window.show_all()
Gtk.main()
def on_button_clicked(self, widget):
canv = canvas.Canvas('tes.pdf')
for i in range(self.textbuffer.get_line_count()):
a = self.textbuffer.get_iter_at_line(i)
b = self.textbuffer.get_iter_at_line(i+1).get_offset()
c = self.textbuffer.get_iter_at_offset(b - 1)
t = self.textbuffer.get_text(a, b, True)
line = 750 - (15 * l)
canv.drawString(40, line, t)
canv.save()
if __name__ == '__main__':
gui = gui()
EDIT:
drahnr suggest to use cairo instead. Okay, I think it's a better idea since reportlab coordinate start from bottom left and cairo coordinate start from top left. Below is my code using cairo.
from gi.repository import Gtk, GdkPixbuf, Gdk
import pango
import cairo
class gui():
def __init__(self):
self.window = Gtk.Window()
self.window.connect('delete-event', Gtk.main_quit)
self.textview = Gtk.TextView()
self.window.add(self.textview)
self.initText()
self.createPDF()
self.window.show_all()
Gtk.main()
def initText(self):
self.tag_bold = self.textview.get_buffer().create_tag("bold", weight=pango.WEIGHT_BOLD)
self.tag_italic = self.textview.get_buffer().create_tag("italic", style=pango.STYLE_ITALIC)
pix = GdkPixbuf.Pixbuf.new_from_file_at_size('baby.png', 50, 50)
tag = [self.tag_bold, self.tag_italic]
self.textview.get_buffer().insert_pixbuf(self.textview.get_buffer().get_end_iter(), pix)
self.textview.get_buffer().insert(self.textview.get_buffer().get_end_iter(), '\n')
for i in range(20):
self.textview.get_buffer().insert_with_tags(self.textview.get_buffer().get_end_iter(), 'line%d' % (i+1), tag[i % 2])
self.textview.get_buffer().insert(self.textview.get_buffer().get_end_iter(), '\n')
def createPDF(self):
line = 30
row = 5
pos = 0
ps = cairo.PDFSurface('tes.pdf', 600, 770)
cr = cairo.Context(ps)
while pos != self.textview.get_buffer().get_end_iter().get_offset():
if self.textview.get_buffer().get_iter_at_offset(pos).has_tag(self.tag_bold):
cr.select_font_face('Times', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
elif self.textview.get_buffer().get_iter_at_offset(pos).has_tag(self.tag_italic):
cr.select_font_face('Times', cairo.FONT_SLANT_ITALIC, cairo.FONT_WEIGHT_NORMAL)
else:
cr.select_font_face('Times', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
t = self.textview.get_buffer().get_slice(self.textview.get_buffer().get_iter_at_offset(pos), self.textview.get_buffer().get_iter_at_offset(pos+1), False)
if t == '\n':
line += 12
row = 5
elif t == unichr(0xFFFC):
pix = self.textview.get_buffer().get_iter_at_offset(pos).get_pixbuf()
Gdk.cairo_set_source_pixbuf(cr, pix, 8 * row, line)
line += pix.get_width()
cr.paint()
else:
cr.move_to(8 * row, line)
cr.show_text(t)
pos=pos+1
row += 1
if __name__ == '__main__':
gui()
literally it's still the same. I should hardcode it to draw everything. and drahnr suggest to use gtk_widget_draw to render it to cairo surface.
This is no python answer, but it might be applicable to your problem too:
Rendering to a cairo_surface_t and use the pdf surface API - how you render your text is up to you, a simple way would be to render the textview via gtk_widget_draw completely.
https://developer.gnome.org/gtk3/stable/GtkWidget.html#gtk-widget-draw.
http://cairographics.org/manual/cairo-PDF-Surfaces.html
Update #2:
The below is incomplete and requires a widget that does the rendering for you and thus does not replace the gtk_widget_draw call. You need to create ctype extension or do the drawing manually.
Source: http://www.stuartaxon.com/2010/02/03/using-cairo-to-generate-svg-in-django/
Update #1:
This function renders an image to a surface (PDF,SVG, whatever your compiled cairo library supports) and stores it to the file dest - for details refer to the manual of the specific function
def draw_render_to_file(dest, Widget, Surface = PDFSurface, width = 100, height = 100)
widget = Widget(Surface)
surface = widget.Surface(dest, width, height)
widget.draw(Context(surface), width, height)
surface.finish()