I had a need for a Qt widget that could display PDF. That turned out
to be surprisingly hard to do. The Qt Wiki has a page on
Handling PDF,
which suggests only two alternatives: QtPDF, which is C++ only
so I would need to write a wrapper to use it with Python (and
then anyone else who used my code would have to compile and install it);
or Poppler. Poppler is a common library on Linux, available as a
package and used for programs like evince, so that seemed like the
best route.
But Python bindings for Poppler are a bit harder to come by.
I found a little
one-page
example using Poppler and Gtk3 via gi.repository ...
but in this case I needed it to work with a Qt5 program, and my
attempts to translate that example to work with Qt were futile.
Poppler's page.render(ctx)
takes a Cairo context,
and Cairo is apparently a Gtk-centered phenomenon: I couldn't find any way
to get a Cairo context from a Qt5 widget, and although I found some
web examples suggesting renderToImage()
,
the Poppler available in gi.repository doesn't have that function.
But it turns out there's another Poppler:
popplerqt5,
available in the Debian package python3-poppler-qt5. That Poppler
does have renderToImage
, and you can take that image and
paint it in a paint() callback or turn it into a pixmap you can use
with a QLabel. Here's the basic sequence:
document = Poppler.Document.load(filename)
document.setRenderHint(Poppler.Document.TextAntialiasing)
page = document.page(pageno)
img = self.page.renderToImage(dpi, dpi)
# Use the rendered image as the pixmap for a label:
pixmap = QPixmap.fromImage(img)
label.setPixmap(pixmap)
The line to set text antialiasing is not optional. Well, theoretically
it's optional; go ahead, try it without that and see for yourself.
It's basically unreadable.
Of course, there are plenty of other details to take care of.
For instance, you can get the size of the rendered image:
size = page.pageSize()
... after which you can use
size.width()
and
size.height()
. They're in points.
There are 72 points per inch, so calculate accordingly in the
dpi
values you pass to
renderToImage if you're targeting a specific
DPI or need it to fit in a specific window size.
Window Resize and Efficient Rendering
Speaking of fitting to a window size, I wanted to resize the content
whenever the window was resized, which meant redefining
resizeEvent(self, event)
on the widget.
Initially my PDFWidget inherited from Qwidget with a custom
paintEvent()
, like this:
# Create self.img once, early on:
self.img = self.page.renderToImage(self.dpi, self.dpi)
def paintEvent(self, event):
qp = QPainter()
qp.begin(self)
qp.drawImage(QPoint(0, 0), self.img)
qp.end()
(Poppler also has a function
page.renderToPainter()
,
but I never did figure out how to get it to do anything useful.)
That worked, but when I added resizeEvent
I got an
infinite loop: paintEvent() called resizeEvent() which triggered
another paintEvent(), ad infinitum. I couldn't find a way around
that (GTK has similar problems -- seems like nearly everything you do
generates another expose event -- but there you can temporarily disable
expose events while you're drawing). So I rewrote my PDFWidget
class to inherit from QLabel instead of QWidget, converted the
QImage to a QPixmap and passed it to self.setPixmap()
.
That let me get rid of the paintEvent() function entirely and
let QLabel handle the painting, which is probably more efficient
anyway.
Showing all pages in a scrolled widget
renderToImage gives you one image corresponding to one page of
the PDF document. More often, you'll want to see the whole document
laid out, with all the pages. So you need a way to stack a bunch of
widgets vertically, one for each page. You can do that with a
QVBoxLayout on a widget inside a QScrollArea.
I haven't done much Qt5 programming, so I wasn't familiar with how
these QVBoxes work. Most toolkits I've worked with have a VBox
container widget to which you add child widgets, but in Qt5, you
create a widget (no particular type -- a QWidget is enough), then
create a layout object that modifies the widget, and add the
sub-widgets to the layout object. There isn't much documentation for
any of this, and very few examples of doing it in Python, so it took
some fiddling to get it working.
Initial Window Size
One last thing: Qt5 doesn't seem to have a concept of desired initial
window size. Most of the examples I found, especially the ones that
use a .ui file, use setGeometry()
; but that requires an
(X, Y) position as well as (width, height), and there's no way to tell
it to ignore the position. That means that instead of letting your
window manager place the window according to your preferences, the
window will insist on showing up at whatever arbitrary place you set
in the code. Worse, most of the Qt5 examples I found online set the
geometry to (0, 0): when I tried that, the window came up with the
widget in the upper left corner of the screen and the window's
titlebar hidden above the top of the screen, so there's no way to move
the window to a better location unless you happen to know your window
manager's hidden key binding for that. (Hint: on many Linux window
managers, hold Alt down and drag anywhere in the window to move it. If
that doesn't work, try holding down the "Windows" key instead of Alt.)
This may explain why I've been seeing an increasing number of these
ill-behaved programs that come up with their titlebars offscreen.
But if you want your programs to be better behaved, it works to
self.resize(width, height)
a widget when you first create it.
The current incarnation of my PDF viewer, set up as a module so you
can import it and use it in other programs, is at
qpdfview.py
on GitHub.
Tags: programming, python, qt
[
19:01 Apr 27, 2018
More programming |
permalink to this entry |
]
A week ago I got back from a trip to the Chiricahua mountains of
southern Arizona, specifically Cave Creek on the eastern side of the range.
The trip was theoretically a hiking trip, but it was also for birding
and wildlife watching -- southern Arizona is near the Mexican border
and gets a lot of birds and other animals not seen in the rest of the
US -- and an excuse to visit a friend who lives near there.
Although it's close enough that it could be driven in one fairly long
day, we took a roundabout 2-day route so we could explore some other
areas along the way that we'd been curious about.
First, we wanted to take a look at the White Mesa Bike Trails
northwest of Albuquerque, near the Ojito Wilderness. We'll be back at
some point with bikes, but we wanted to get a general idea of the
country and terrain. The Ojito, too, looks like it might be worth
a hiking trip, though it's rather poorly signed: we saw several kiosks
with maps where the "YOU ARE HERE" was clearly completely misplaced.
Still, how can you not want to go back to a place where the two main
trails are named Seismosaurus and Hoodoo?
The route past the Ojito also led past Cabezon Peak, a volcanic neck
we've seen from a long distance away and wanted to see closer. It's
apparently possible to climb it but we're told the top part is fairly
technical, more than just a hike.
Finally, we went up and over Mt Taylor, something we've been meaning
to do for many years. You can drive fairly close to the top, but
this being late spring, there was still snow on the upper part of
the road and our Rav4's tires weren't up to the challenge. We'll
go back some time and hike all the way to the top.
We spent the night in Grants, then the following day, headed down
through El Malpais, stopping briefly at the beautiful Sandstone Overlook,
then down through the Datil and Mogollon area. We wanted to take a
look at a trail called the Catwalk, but when we got there, it was
cold, blustery, and starting to rain and sleet. So we didn't hike the
Catwalk this time, but at least we got a look at the beginning of it,
then continued down through Silver City and thence to I-10,
where just short of the Arizona border we were amused by the
Burma
Shave dust storm signs about which I already wrote.
At Cave Creek
Cave Creek Ranch, in Portal, AZ,
turned out to be a lovely place to stay, especially
for anyone interested in wildlife. I saw several "life birds" and
mammals, plus quite a few more that I'd seen at some point but had
never had the opportunity to photograph. Even had we not been hiking,
just hanging around the ranch watching the critters was a lot of fun.
They charge $5 for people who aren't staying there to come and sit in
the feeder area; I'm not sure how strictly they enforce it, but given
how much they must spend on feed, it would be nice to help support them.
The bird everyone was looking for was the Elegant Trogon. Supposedly
one had been seen recently along the creekbed, and we all wanted to
see it.
They also had a nifty suspension bridge for pedestrians crossing a dry
(this year) arroyo over on another part of the property. I guess I was
so busy watching the critters that I never went wandering around, and
I would have missed the bridge entirely had Dave not pointed it out
to me on the last day.
The only big hike I did was the Burro Trail to Horseshoe Pass, about
10 miles and maybe 1800 feet of climbing. It started with a long hike
up the creek, during which everybody had eyes and ears trained on the
sycamores (we were told the trogon favored sycamores). No trogon.
But it was a pretty hike, and once we finally started climbing out of
the creekbed there were great views of the soaring cliffs above Cave
Creek Canyon. Dave opted to skip the upper part of the trail to the
saddle; I went, but have to admit that it was mostly just more of the
same, with a lot of scrambling and a few difficult and exposed traverses.
At the time I thought it was worth it, but by the time we'd slogged
all the way back to the cars I was doubting that.
On the second day the group went over the Chiricahuas to Chiricahua
National Monument, on the other side. Forest road 42 is closed in
winter, but we'd been told that it was open now since the winter had
been such a dry one, and it wasn't a particularly technical road,
certainly easy in the Rav4. But we had plans to visit our friend over
at the base of the next mountain range west, so we just made a quick
visit to the monument, did a quick hike around the nature trail and
headed on.
Back with the group at Cave Creek on Thursday, we opted for a shorter,
more relaxed hike in the canyon to Ash Spring rather than the brutal
ascent to Silver Peak. In the canyon, maybe we'd see the trogon!
Nope, no trogon. But it was a very pleasant hike, with our first
horned lizard ("horny toad") spotting of the year, a couple of other
lizards, and some lovely views.
Critters
We'd been making a lot of trogon jokes over the past few days, as we
saw visitor after visitor trudging away muttering about not having
seen one. "They should rename the town of Portal to Trogon, AZ." "They
should rename that B&B Trogon's Roost Bed and Breakfast." Finally,
at the end of Thursday's hike, we stopped in at the local ranger
station, where among other things (like admiring their caged gila
monster) we asked about trogon sightings. Turns out the last one to be
seen had been in November. A local thought maybe she'd heard one in
January. Whoever had relayed the rumor that one had been seen recently
was being wildly optimistic.
Fortunately, I'm not a die-hard birder and I didn't go there
specifically for the trogon. I saw lots of good birds and some mammals
I'd never seen before
(full
list), like a coatimundi (I didn't realize those ever came up to
the US) and a herd (pack? flock?) of javalinas. And white-tailed deer
-- easterners will laugh, but those aren't common anywhere I've lived
(mule deer are the rule in California and Northern New Mexico). Plus
some good hikes with great views, and a nice visit with our friend. It
was a good trip.
On the way home, again we took two days for the opportunity to visit
some places we hadn't seen. First, Cloudcroft, NM: a place we'd heard
a lot about because a lot of astronomers retire there. It's high in
the mountains and quite lovely, with lots of hiking trails in the
surrounding national forest. Worth a visit some time.
From Cloudcroft we traveled through the Mescalero Apache reservation,
which was unexpectedly beautiful, mountainous and wooded and dotted
with nicely kept houses and ranches, to Ruidoso, a nice little town
where we spent the night.
Lincoln
Our last stop, Saturday morning, was Lincoln, site of the Lincoln
County War (think Billy the Kid). The whole tiny town is set up as a
tourist attraction, with old historic buildings ... that were all
closed. Because why would any tourists be about on a beautiful
Saturday in spring? There were two tiny museums, one at each end of
town, which were open, and one of them tried to entice us into paying
the entrance fee by assuring us that the ticket was good for all the
sites in town. Might have worked, if we hadn't already walked the
length of the town peering into windows of all the closed sites. Too
bad -- some of them looked interesting, particularly the general store.
But we enjoyed our stroll through the town, and we got a giggle out of
the tourist town being closed on Saturday -- their approach to tourism
seems about as effective as Los Alamos'.
Photos from the trip are at
Cave Creek and the Chiricahuas.
Tags: travel, birds, hiking
[
10:04 Apr 05, 2018
More travel |
permalink to this entry |
]