diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba1cf8c --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +gpdfx-ng - a graphical tool to extract parts of a PDF as a PDF +=============================== + +`gpdfx-ng` is a GTK application written in Python using the Poppler library +to render the PDF. It uses pdfTeX and PDFCrop to create the extracted +PDF. + +# Features +My improvements to the ancient `gpdfx`: + +- Add a "Open" button (open file). +- Add a "Auto Fit" botton. It helps to fit the pdf file according to the size of the window. +- Add a button "Viewport". It copies the "viewport" of the selected area to the clipboard. +- Add a status bar. + +# Install +Dependency +- Python2 or Python3 +- poppler-glib +- python-gobject +- gtk3 +- python-cairo + +The one line script `install.sh` helps to install (copy) `gpdfx-ng` to your PATH, +in you want. + +# Usage +- Run `gpdfx-ng` and open a pdf file. You can also pass a file name the through command line argument. +- Select an area. +- Export the selection to a pdf file, or copy the "viewport" to the clipboard. + +## Viewport +If you want to insert the selected area of your pdf file in a LaTeX document, you may find the following codes useful. +``` + \begin{figure}[ht] + \includegraphics[clip, page=1, viewport=1 2 3 4, + width=1.0\textwidth, fbox]{paper.pdf} + \end{figure} +``` + +Upstream +-------- +- https://github.com/lehner/gpdfx +- Author: Christoph Lehner (clehner // users.sourceforge.net) diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..710e52f --- /dev/null +++ b/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -x +sudo install -Dm 755 src/gpdfx-ng /usr/local/bin/gpdfx-ng diff --git a/readme.txt b/readme.txt deleted file mode 100644 index 4ce7ed4..0000000 --- a/readme.txt +++ /dev/null @@ -1,10 +0,0 @@ - -gpdfx - a graphical tool to extract parts of a PDF as a PDF ------------------------------------------------------------------------------- - -gpdfx is a GTK application written in Python using the Poppler library -to render the PDF. It uses pdfTeX and PDFCrop to create the extracted -PDF. - -Author - * Christoph Lehner (clehner // users.sourceforge.net) diff --git a/src/clipdfx b/src/clipdfx deleted file mode 100755 index e722bc6..0000000 --- a/src/clipdfx +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash -# -# Copyright (C) 2011 Christoph Lehner, Nathan Goldschmidt -# -# v1[Lehner]: Original version -# v2[Goldschmidt]: Use ghostscript to remove invisible content, format FN -# v3[Lehner]: Streamline code -# -# All programs in this directory and subdirectories are published under the GNU -# General Public License as described below. -# -# 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., 59 Temple -# Place, Suite 330, Boston, MA 02111-1307 USA -# -# Further information about the GNU GPL is available at: -# http://www.gnu.org/copyleft/gpl.ja.html - -# -# Assign arguments -# -fn=$1 -fnout=$2 -page=$3 -left=$4 -top=$5 -right=$6 -bottom=$7 - -# -# Create temporary directory -# -tdir=$(mktemp -d) -mkdir -p $tdir - -# -# Use a simple filename for LaTeX with .pdf extension -# -cp -f "$fn" $tdir/i.pdf - -# -# Use LaTeX to extract the -# -cat > $tdir/o.tex <self.width: - self.sel_x=self.width - if self.sel2_x>self.width: - self.sel2_x=self.width - if self.sel_y>self.height: - self.sel_y=self.height - if self.sel2_y>self.height: - self.sel2_y=self.height - - if self.sel_x<0: - self.sel_x=0 - if self.sel2_x<0: - self.sel2_x=0 - if self.sel_y<0: - self.sel_y=0 - if self.sel2_y<0: - self.sel2_y=0 - - def on_btn_down(self, widget, event): - self.sel_x=event.x/self.scale - self.sel_y=event.y/self.scale - self.sel2_x=event.x/self.scale - self.sel2_y=event.y/self.scale - self.sel_pos_clip() - self.sel = True - - def on_btn_up(self, widget, event): - self.sel2_x=event.x/self.scale - self.sel2_y=event.y/self.scale - self.sel_pos_clip() - self.dwg.queue_draw() - - def on_mouse_move(self, widget, event): - self.sel2_x=event.x/self.scale - self.sel2_y=event.y/self.scale - self.sel_pos_clip() - self.dwg.queue_draw() - - def on_changed(self, widget): - self.n_page = widget.get_value_as_int() - self.current_page = self.document.get_page(widget.get_value_as_int()-1) - self.width, self.height = self.current_page.get_size() - self.dwg.set_size_request(int(self.width*self.scale), - int(self.height*self.scale)) - self.dwg.queue_draw() - - def on_scale_changed(self, widget): - self.scale = widget.get_value_as_int()/100.0 - self.dwg.set_size_request(int(self.width*self.scale), - int(self.height*self.scale)) - self.dwg.queue_draw() - - def cr_draw(self,cr,width,height,scale): - if scale != 1: - cr.scale(scale, scale) - cr.set_source_rgb(1, 1, 1) - cr.rectangle(0, 0, width, height) - cr.fill() - self.current_page.render(cr) - - if self.sel: - cr.set_source_rgba(0, 0, 0.5, 0.9) - cr.set_line_width(1) - cr.rectangle(self.sel_x,self.sel_y,self.sel2_x-self.sel_x, - self.sel2_y-self.sel_y) - cr.stroke_preserve() - cr.set_source_rgba(0, 0, 1, 0.2) - cr.fill() - - def on_expose(self, widget, event): - cr = widget.window.cairo_create() - self.cr_draw(cr,self.width,self.height,self.scale) - - def on_clear_sel(self, widget): - self.sel = False - self.dwg.queue_draw() - - def on_export(self, widget): - if self.sel: - - if self.sel_x>self.sel2_x: - tmp=self.sel_x - self.sel_x=self.sel2_x - self.sel2_x=tmp - - if self.sel_y>self.sel2_y: - tmp=self.sel_y - self.sel_y=self.sel2_y - self.sel2_y=tmp - - ch = gtk.FileChooserDialog("Filename for selection PDF",None, - gtk.FILE_CHOOSER_ACTION_SAVE, - (gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL, - gtk.STOCK_OPEN,gtk.RESPONSE_OK)) - ch.set_do_overwrite_confirmation(True) - fnout = None - if ch.run() == gtk.RESPONSE_OK: - fnout = ch.get_filename() - ch.destroy() - - if fnout!=None: - exportPdf(self.fn,fnout,self.n_page, - self.sel_x,self.sel_y, - self.width-self.sel2_x, - self.height-self.sel2_y) - - def main(self): - gtk.main() - - -def exportPdf(fn,fnout,npage,left,top,right,bottom): - os.system("clipdfx \"" + fn + "\" \"" + fnout + "\" " + str(npage) + " " - + str(left) + " " + str(top) + " " + str(right) + " " - + str(bottom)) - -if __name__ == '__main__': - try: - if len(sys.argv)==2: - pop = Poprender() - pop.main() - else: - print "Usage:" - print os.path.basename(sys.argv[0]) + " filename.pdf" - except KeyboardInterrupt: - __name__ diff --git a/src/gpdfx-ng b/src/gpdfx-ng new file mode 100755 index 0000000..7eba9f5 --- /dev/null +++ b/src/gpdfx-ng @@ -0,0 +1,374 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Christoph Lehner, Nathan Goldschmidt +# +# v1[Lehner]: Original version +# v2[Goldschmidt]: Use expanduser, expandvars. Use self.fn instead of fn. +# v3[Lehner]: Use self.fn in argument of export_pdf. +# +# Copyright (C) 2016 Yishi Lin +# +# All programs in this directory and subdirectories are published under the GNU +# General Public License as described below. +# +# 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., 59 Temple +# Place, Suite 330, Boston, MA 02111-1307 USA +# +# Further information about the GNU GPL is available at: +# http://www.gnu.org/copyleft/gpl.ja.html + +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('Poppler', '0.18') +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import Poppler +import sys +import os +import cairo +import tempfile +import shutil + +class PopRender(object): + def __init__(self): + # Initialize variables + self.sel_x = 0 + self.sel2_x = 0 + self.sel_y = 0 + self.sel2_y = 0 + self.btn_down = False + self.sel = False + self.scale = 1 + self.width = 1 + self.height = 1 + self.opened_file = False + self.n_page = 1 + self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + + # Initialize window + self.win = Gtk.Window() + self.win.set_default_size(600, 600) + self.win.set_title("gpdfx") + self.win.connect("delete-event", Gtk.main_quit) + + vbox = Gtk.VBox(False, 0) + + # Put buttons in a flowbox + flowbox = Gtk.FlowBox() + flowbox.set_valign(Gtk.Align.START) + flowbox.set_min_children_per_line(5) + flowbox.set_max_children_per_line(30) + flowbox.set_selection_mode(Gtk.SelectionMode.NONE) + vbox.pack_start(flowbox, False, False, 0) + + # Open file + b_open_file = Gtk.Button("Open") + b_open_file.connect("clicked", self.on_file_clicked) + flowbox.add(b_open_file) + + # Page up/down + adjust = Gtk.Adjustment(self.n_page, 1, self.n_page, 1, 5) + self.page_selector = Gtk.SpinButton() + self.page_selector.set_adjustment(adjust) + self.page_selector.connect("value-changed", self.on_page_changed) + lab = Gtk.Label('Page') + flowbox.add(lab) + flowbox.add(self.page_selector) + + # Zoom in/out + adjust = Gtk.Adjustment(self.scale * 100, 25, 400, 25, 100) + scale_selector = Gtk.SpinButton() + scale_selector.set_adjustment(adjust) + scale_selector.connect("value-changed", self.on_scale_changed) + lab = Gtk.Label('Zoom') + flowbox.add(lab) + flowbox.add(scale_selector) + + # Auto fit + b_auto_fit = Gtk.Button('Auto Fit') + b_auto_fit.connect("clicked", self.on_auto_fit) + flowbox.add(b_auto_fit) + + # Export to PDF + b_export_pdf = Gtk.Button('Selection to PDF') + b_export_pdf.connect("clicked", self.on_export) + flowbox.add(b_export_pdf) + + # Copy area coord + b_copy_coord = Gtk.Button('Viewport (LaTeX)') + b_copy_coord.connect("clicked", self.on_copy_coord) + flowbox.add(b_copy_coord) + + # Scrolled windows (display PDF) + self.sw = Gtk.ScrolledWindow() + self.sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.dwg = Gtk.DrawingArea() + self.dwg.set_size_request(int(self.width), int(self.height)) + self.dwg.connect("draw", self.on_expose) + + # Connect events + self.sw.connect("button-press-event", self.on_btn_down) + self.sw.connect("button-release-event", self.on_btn_up) + self.sw.connect("motion-notify-event", self.on_mouse_move) + + eventbox = Gtk.EventBox() + eventbox.add(self.dwg) + self.sw.add_with_viewport(eventbox) + vbox.pack_start(self.sw, True, True, 0) + + # The statusbar + self.statusbar = Gtk.Statusbar() + self.context_id = self.statusbar.get_context_id("example") + self.statusbar.push(self.context_id, "Please open a file...") + vbox.pack_start(self.statusbar, False, False, 0) + + # Try to open argv[1] + if len(sys.argv)==2: + self.fn = os.path.expanduser(sys.argv[1]) + self.fn = os.path.expandvars(self.fn) + self.fn = os.path.abspath(self.fn) + self.open_file() + + # Start window + self.win.add(vbox) + self.win.show_all() + + def open_file(self): + # Open document + uri = "file://" + self.fn + print("File selected: " + uri) + try: + self.document = Poppler.Document.new_from_file(uri, None) + except: + sys.exit(1) + + self.n_pages = self.document.get_n_pages() + self.current_page = self.document.get_page(0) + self.width, self.height = self.current_page.get_size() + + # Set window title + self.win.set_title(self.fn) + + # Set page range + self.n_page = 1 + adjust = Gtk.Adjustment(1, 1, self.n_pages, 1, 5) + self.page_selector.set_adjustment(adjust) + + # Load pdf + self.scale = min(self.win.get_size().width / self.width, + self.win.get_size().height / self.height) + self.dwg.set_size_request( + int(self.width * self.scale), int(self.height * self.scale)) + self.dwg.queue_draw() + self.dwg.modify_bg(Gtk.StateFlags.NORMAL, + Gdk.Color(50000, 50000, 50000)) + self.opened_file = True + + # Update status bar + self.statusbar.push(self.context_id, "File: {}".format(self.fn)) + + def on_file_clicked(self, widget): + dialog = Gtk.FileChooserDialog( + "Please choose a file", None, Gtk.FileChooserAction.OPEN, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, + Gtk.ResponseType.OK)) + + response = dialog.run() + if response == Gtk.ResponseType.OK: + self.fn = dialog.get_filename() + self.open_file() + + elif response == Gtk.ResponseType.CANCEL: + print("Cancel clicked") + + dialog.destroy() + + def sel_pos_clip(self): + self.sel_x = min(self.sel_x, self.width) + self.sel_y = min(self.sel_y, self.height) + self.sel2_x = min(self.sel2_x, self.width) + self.sel2_y = min(self.sel2_y, self.height) + + self.sel_x = max(self.sel_x, 0) + self.sel_y = max(self.sel_y, 0) + self.sel2_x = max(self.sel2_x, 0) + self.sel2_y = max(self.sel2_y, 0) + + def on_btn_down(self, widget, event): + self.sel_x = event.x / self.scale + self.sel_y = event.y / self.scale + self.sel2_x = event.x / self.scale + self.sel2_y = event.y / self.scale + self.sel_pos_clip() + self.sel = True + self.btn_down = True + + def on_btn_up(self, widget, event): + self.sel2_x = event.x / self.scale + self.sel2_y = event.y / self.scale + self.sel_pos_clip() + self.dwg.queue_draw() + self.btn_down = False + self.statusbar.push( + self.context_id, + 'Selected area: {:.2f} {:.2f} {:.2f} {:.2f}'.format( + self.sel_x, self.sel_y, self.sel2_x, self.sel2_y)) + + def on_mouse_move(self, widget, event): + if self.opened_file and self.btn_down: + self.sel2_x = event.x / self.scale + self.sel2_y = event.y / self.scale + self.sel_pos_clip() + self.dwg.queue_draw() + self.statusbar.push( + self.context_id, + 'Selected area: {:.2f} {:.2f} {:.2f} {:.2f}'.format( + self.sel_x, self.sel_y, self.sel2_x, self.sel2_y)) + + def on_page_changed(self, widget): + if self.opened_file: + self.n_page = widget.get_value_as_int() + page = widget.get_value_as_int() - 1 + self.current_page = self.document.get_page(page) + self.width, self.height = self.current_page.get_size() + self.dwg.set_size_request( + int(self.width * self.scale), int(self.height * self.scale)) + self.dwg.queue_draw() + + def on_auto_fit(self, widget): + if self.opened_file: + self.scale = min(self.win.get_size().width / self.width, + self.win.get_size().height / self.height) + self.dwg.set_size_request( + int(self.width * self.scale), int(self.height * self.scale)) + self.dwg.queue_draw() + + def on_scale_changed(self, widget): + if self.opened_file: + self.scale = widget.get_value_as_int() / 100.0 + self.dwg.set_size_request( + int(self.width * self.scale), int(self.height * self.scale)) + self.dwg.queue_draw() + + def cr_draw(self, cr, width, height, scale): + if self.opened_file: + if scale != 1: + cr.scale(scale, scale) + cr.set_source_rgb(1, 1, 1) + cr.rectangle(0, 0, width, height) + cr.fill() + self.current_page.render(cr) + + if self.sel: + cr.set_source_rgba(0, 0, 0.5, 0.9) + cr.set_line_width(1) + cr.rectangle(self.sel_x, self.sel_y, self.sel2_x - self.sel_x, + self.sel2_y - self.sel_y) + cr.stroke_preserve() + cr.set_source_rgba(0, 0, 1, 0.2) + cr.fill() + + def on_expose(self, widget, event): + cr = widget.get_property('window').cairo_create() + self.cr_draw(cr, self.width, self.height, self.scale) + + def on_export(self, widget): + if self.sel: + + if self.sel_x > self.sel2_x: + tmp = self.sel_x + self.sel_x = self.sel2_x + self.sel2_x = tmp + + if self.sel_y > self.sel2_y: + tmp = self.sel_y + self.sel_y = self.sel2_y + self.sel2_y = tmp + + ch = Gtk.FileChooserDialog( + "Filename for selection PDF", None, Gtk.FileChooserAction.SAVE, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, + Gtk.ResponseType.OK)) + ch.set_do_overwrite_confirmation(True) + fnout = None + if ch.run() == Gtk.ResponseType.OK: + fnout = ch.get_filename() + ch.destroy() + if fnout != None: + export_pdf(self.fn, fnout, self.n_page, self.sel_x, self.sel_y, + self.width - self.sel2_x, self.height - self.sel2_y) + + self.statusbar.push(self.context_id, + 'Export to file: {}'.format(fnout)) + + def on_copy_coord(self, widget): + # Here: top left corner = (0,0) + llx = min(self.sel_x, self.sel2_x) + lly = max(self.sel_y, self.sel2_y) + urx = max(self.sel_x, self.sel2_x) + ury = min(self.sel_y, self.sel2_y) + # Viewport in latex: lower left corner = (0,0) + viewport = '{:.2f} {:.2f} {:.2f} {:.2f}'.format(llx, self.height - lly, + urx, self.height - ury) + self.clipboard.set_text(viewport, -1) + self.statusbar.push(self.context_id, + 'Copied viewport (LaTeX): ' + viewport) + + def main(self): + Gtk.main() + + +def export_pdf(fn, fnout, npage, left, top, right, bottom): + # Check current working directory. + current_dir = os.getcwd() + + # Create a temporary directory + temp_dir = tempfile.mkdtemp() + + # Use a simple filename for LaTeX with .pdf extension + shutil.copy(fn, os.path.join(temp_dir, 'i.pdf')) + + # cd temp dir + os.chdir(temp_dir) + + # Use LaTeX to extract the + lines = """ +\\documentclass{{standalone}} +\\usepackage{{graphicx}} +\\begin{{document}} +\\includegraphics[page={0},trim={1} {2} {3} {4},clip]{{{5}/i.pdf}} +\\end{{document}} + """.format(npage, left, bottom, right, top, temp_dir) + + print(left,bottom,right,top) + + with open('o.tex', 'w') as output: + output.write(lines) + os.system('pdflatex ' + 'o.tex') + os.system('pdfcrop ' + 'o.pdf') + + # Use ghostscript to remove invisible content + os.system('gs -q -sDEVICE=pdfwrite -dNOPAUSE -dBATCH ' + + '-sOutputFile=' + fnout + ' -f o-crop.pdf') + + # cd back + os.chdir(current_dir) + shutil.rmtree(temp_dir) + +if __name__ == '__main__': + try: + pop = PopRender() + pop.main() + except KeyboardInterrupt: + __name__