commit f3d8e3968b8888c94e1d90355c7516e1c9e76427 Author: Trullemans Gregory Date: Wed Jul 28 22:28:27 2021 +0200 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..58fecf4 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# rebook + +rebook is a Tcl/Tk based GUI for the PDF conversion tool, k2pdfopt. + +rebook depends on python3 and Tcl/Tk 8.6. Tck/Tk with version lower than 8.6 doesn't work. + +To check if the dependencies are correctly installed, just run `python3 -m tkinter`. + +To run rebook, put k2pdfopt binary and rebook.py under the same directory and run `python3 rebook.py` + +rebook is still under developing, feel free to report any issue. diff --git a/conversion.png b/conversion.png new file mode 100644 index 0000000..fc8c9c5 Binary files /dev/null and b/conversion.png differ diff --git a/k2pdfopt b/k2pdfopt new file mode 100755 index 0000000..6965c06 Binary files /dev/null and b/k2pdfopt differ diff --git a/log.png b/log.png new file mode 100644 index 0000000..048afab Binary files /dev/null and b/log.png differ diff --git a/rebook.py b/rebook.py new file mode 100644 index 0000000..fe5fc97 --- /dev/null +++ b/rebook.py @@ -0,0 +1,2272 @@ +#!/usr/bin/python + +from threading import Thread +from tkinter import * +from tkinter.ttk import * +import asyncio +import glob +import json +import os +import subprocess as sub +import tkinter.filedialog +import tkinter.messagebox +import tkinter.scrolledtext as scrltxt + + +def check_page_nums(input_pages): + page_num_list = re.split(',|-|o|e', input_pages) + + for page_num in page_num_list: + if len(page_num) > 0 and not page_num.isdigit(): + return FALSE + return TRUE + +# GUI construction + +root = Tk() + +# screen_width = root.winfo_screenwidth() +# screen_height = root.winfo_screenheight() + +# root.resizable(FALSE, FALSE) + +root.title('rebook') + +# main tab + +baseTab = Notebook(root) + +conversionTab = Frame(baseTab) +baseTab.add(conversionTab, text='Conversion') + +logTab = Frame(baseTab) +baseTab.add(logTab, text='Log') + +baseTab.pack(expand=1, fill='both') + +# menu + +menu_bar = Menu(root) +root['menu'] = menu_bar + +def on_command_about_box_cb(): + theMessage = \ + '''rebook + +TclTk GUI for k2pdfopt by Pu Wang + +The source code can be found at: +http://github.com/pwang7/rebook/rebook.py''' + + tkinter.messagebox.showinfo(message=theMessage) + +menu_file = Menu(menu_bar) +menu_bar.add_cascade(menu=menu_file, label='File') +menu_file.add_command(label='About', command=on_command_about_box_cb) + +# root.createcommand('tkAboutDialog', on_command_about_box_cb) +# root.createcommand('::tk::mac::ShowHelp', on_command_about_box_cb) + +# global variables and functions + +k2pdfopt_path = './k2pdfopt' +custom_preset_file_path = 'rebook_preset.json' + +def check_k2pdfopt_path_exists(): + if not os.path.exists(k2pdfopt_path): + tkinter.messagebox.showerror( + message='Failed to find k2pdfopt, ' + + 'please put it under the same directory ' + + 'as rebook and then restart.' + ) + quit() + +k2pdfopt_cmd_args = {} + +def update_cmd_arg_entry_strvar(): + global strvarCmdArgs + + strvarCmdArgs.set(generate_cmd_arg_str()) + +def add_or_update_one_cmd_arg(arg_key, arg_value): + global k2pdfopt_cmd_args + + k2pdfopt_cmd_args[arg_key] = arg_value + +def remove_one_cmd_arg(arg_key): + global k2pdfopt_cmd_args + + previous = k2pdfopt_cmd_args.pop(arg_key, None) + + return previous + +def load_custom_preset(): + global strvarOutputFilePath + + if os.path.exists(custom_preset_file_path): + with open(custom_preset_file_path) as preset_file: + dict_to_load = json.load(preset_file) + + if dict_to_load: + log_string('Load Preset: ' + str(dict_to_load)) + + initialize_vars(dict_to_load) + + return TRUE + + return FALSE + +def log_string(str_line): + global stdoutText + + log_content = str_line.strip() + if len(log_content) > 0: + stdoutText.config(state=NORMAL) + + print('=== ' + log_content) # TODO: remove print + + stdoutText.insert(END, log_content + '\n') + stdoutText.config(state=DISABLED) + +def clear_logs(): + stdoutText.config(state=NORMAL) + stdoutText.delete(1.0, END) + stdoutText.config(state=DISABLED) + +def initialize_vars(dict_vars): + global k2pdfopt_cmd_args + + for k, v in dict_vars.items(): + for i in range(len(v)): + arg_var_map[k][i].set(v[i]) + + k2pdfopt_cmd_args = {} + + for cb_func in arg_cb_map.values(): + if cb_func is not None: + cb_func() + # must be after loading preset values + update_cmd_arg_entry_strvar() + +def restore_default_values(): + clear_logs() + + remove_preview_img_and_clear_canvas() + + for sv in string_var_list: + sv.set('') + + for bv in bool_var_list: + bv.set(FALSE) + + for b in combo_box_list: + b.current(0) + + initialize_vars(default_var_map) + +def start_loop(loop): + asyncio.set_event_loop(loop) + loop.run_forever() + +thread_loop = asyncio.get_event_loop() +run_loop_thread = Thread( + target=start_loop, + args=(thread_loop,), + daemon=TRUE, +) +run_loop_thread.start() + +background_process = None +background_future = None + +device_argument_map = { + 0: 'k2', + 1: 'dx', + 2: 'kpw', + 3: 'kp2', + 4: 'kp3', + 5: 'kv', + 6: 'ko2', + 7: 'pb2', + 8: 'nookst', + 9: 'kbt', + 10: 'kbg', + 11: 'kghd', + 12: 'kghdfs', + 13: 'kbm', + 14: 'kba', + 15: 'kbhd', + 16: 'kbh2o', + 17: 'kbh2ofs', + 18: 'kao', + 19: 'nex7', + 20: None, +} + +device_choice_map = { + 0: 'Kindle 1-5', + 1: 'Kindle DX', + 2: 'Kindle Paperwhite', + 3: 'Kindle Paperwhite 2', + 4: 'Kindle Paperwhite 3', + 5: 'Kindle Voyage/PW3+/Oasis', + 6: 'Kindle Oasis 2', + 7: 'Pocketbook Basic 2', + 8: 'Nook Simple Touch', + 9: 'Kobo Touch', + 10: 'Kobo Glo', + 11: 'Kobo Glo HD', + 12: 'Kobo Glo HD Full Screen', + 13: 'Kobo Mini', + 14: 'Kobo Aura', + 15: 'Kobo Aura HD', + 16: 'Kobo H2O', + 17: 'Kobo H2O Full Screen', + 18: 'Kobo Aura One', + 19: 'Nexus 7', + 20: 'Other (specify width & height)', +} + +mode_argument_map = { + 0: 'def', + 1: 'copy', + 2: 'fp', + 3: 'fw', + 4: '2col', + 5: 'tm', + 6: 'crop', + 7: 'concat', +} + +mode_choice_map = { + 0: 'Default', + 1: 'Copy', + 2: 'Fit Page', + 3: 'Fit Width', + 4: '2 Columns', + 5: 'Trim Margins', + 6: 'Crop', + 7: 'Concat', +} + +unit_argument_map = { + 0: 'in', + 1: 'cm', + 2: 's', + 3: 't', + 4: 'p', + 5: 'x', +} + +unit_choice_map = { + 0: 'Inches', + 1: 'Centimeters', + 2: 'Source Page Size', + 3: 'Trimmed Source Region Size', + 4: 'Pixels', + 5: 'Relative to the OCR Text Layer', +} + +def generate_cmd_arg_str(): + must_have_args = '-a- -ui- -x' + + device_arg = k2pdfopt_cmd_args.pop(device_arg_name, None) + if device_arg is None: + width_arg = k2pdfopt_cmd_args.pop(width_arg_name) + height_arg = k2pdfopt_cmd_args.pop(height_arg_name) + + mode_arg = k2pdfopt_cmd_args.pop(conversion_mode_arg_name) + + arg_list = [mode_arg] + list(k2pdfopt_cmd_args.values()) + + k2pdfopt_cmd_args[conversion_mode_arg_name] = mode_arg + if device_arg is not None: + arg_list.append(device_arg) + + k2pdfopt_cmd_args[device_arg_name] = device_arg + else: + arg_list.append(width_arg) + arg_list.append(height_arg) + + k2pdfopt_cmd_args[width_arg_name] = width_arg + k2pdfopt_cmd_args[height_arg_name] = height_arg + + arg_list.append(must_have_args) + + log_string('Generate Argument List: ' + str(arg_list)) + + cmd_arg_str = ' '.join(arg_list) + + return cmd_arg_str + +def convert_pdf_file(output_arg): + check_k2pdfopt_path_exists() + + async def async_run_cmd_and_log(exec_cmd): + global background_process + + executed = exec_cmd.strip() + + def log_bytes(log_btyes): + log_string(log_btyes.decode('utf-8')) + + log_string(executed) + + p = await asyncio.create_subprocess_shell( + executed, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + background_process = p + + while True: + line = await p.stdout.readline() + + log_bytes(line) + + if not line: + break + if line == '' and p.returncode is not None: + break + + input_pdf_path = strvarFilePath.get().strip() + if ' ' in input_pdf_path: + # in case the file name contains space + input_pdf_path = '\"' + input_pdf_path + '\"' + + executed = ' '.join([ + k2pdfopt_path, + input_pdf_path, + output_arg, + generate_cmd_arg_str(), + ]) + future = asyncio.run_coroutine_threadsafe( + async_run_cmd_and_log(executed), + thread_loop, + ) + return future + +def check_pdf_conversion_done(): + if (background_future is None) or (background_future.done()): + if ((background_process is None) or + (background_process.returncode is not None)): + return TRUE + + tkinter.messagebox.showerror( + message='Background Conversion Not Finished Yet! Please Wait.', + ) + return FALSE + +# conversion tab +conversion_tab_left_part_column_num = 0 +conversion_tab_left_part_row_num = -1 + +# required inputs +conversion_tab_left_part_row_num += 1 + +device_arg_name = '-dev' # -dev +width_arg_name = '-w' # -w [in|cm|s|t|p] +height_arg_name = '-h' # -h [in|cm|s|t|p|x] +conversion_mode_arg_name = '-mode' # -mode +output_path_arg_name = '-o' # -o +output_pdf_suffix = '-output.pdf' +screen_unit_prefix = '-screen_unit' + +strvarFilePath = StringVar() +strvarDevice = StringVar() +strvarConversionMode = StringVar() +strvarScreenUnit = StringVar() +strvarScreenWidth = StringVar() +strvarScreenHeight = StringVar() + +requiredInputFrame = Labelframe(conversionTab, text='Required Inputs') +requiredInputFrame.grid( + column=conversion_tab_left_part_column_num, + row=conversion_tab_left_part_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +required_frame_row_num = 0 + +inputPathEntry = Entry( + requiredInputFrame, + state='readonly', + textvariable=strvarFilePath, +) +inputPathEntry.grid( + column=0, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +def on_command_open_pdf_file_cb(): + formats = [('PDF files', '*.pdf')] + + filename = tkinter.filedialog.askopenfilename( + parent=root, + filetypes=formats, + title='Choose a file', + ) + + if filename is not None and len(filename.strip()) > 0: + strvarFilePath.set(filename) + (base_path, file_ext) = os.path.splitext(filename) + strvarOutputFilePath.set(base_path + output_pdf_suffix) + +openButton = Button( + requiredInputFrame, + text='Open PDF File', + command=on_command_open_pdf_file_cb, +) +openButton.grid( + column=1, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +required_frame_row_num += 1 + +deviceText = Label(requiredInputFrame, text='Device:') +deviceText.grid( + column=0, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +def update_device_unit_width_height(): + if deviceComboBox.current() != 20: # non-other type + deviceType = device_argument_map[deviceComboBox.current()] + arg = device_arg_name + ' ' + deviceType + add_or_update_one_cmd_arg(device_arg_name, arg) + + remove_one_cmd_arg(width_arg_name) + remove_one_cmd_arg(height_arg_name) + else: + screen_unit = unit_argument_map[unitComboBox.current()] + + width_arg = ( + width_arg_name + ' ' + + strvarScreenWidth.get().strip() + screen_unit + ) + add_or_update_one_cmd_arg(width_arg_name, width_arg) + + height_arg = ( + height_arg_name + ' ' + + strvarScreenHeight.get().strip() + screen_unit + ) + add_or_update_one_cmd_arg(height_arg_name, height_arg) + + remove_one_cmd_arg(device_arg_name) + +def on_bind_event_device_unit_cb(e=None): + update_device_unit_width_height() + +deviceComboBox = Combobox( + requiredInputFrame, + state='readonly', + textvariable=strvarDevice, +) +deviceComboBox['values'] = list(device_choice_map.values()) +deviceComboBox.current(0) +deviceComboBox.bind('<>', on_bind_event_device_unit_cb) +deviceComboBox.grid( + column=1, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +required_frame_row_num += 1 + +unitTextLabel = Label(requiredInputFrame, text='Unit:') +unitTextLabel.grid( + column=0, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +unitComboBox = Combobox( + requiredInputFrame, + state='readonly', + textvariable=strvarScreenUnit, +) +unitComboBox['values'] = list(unit_choice_map.values()) +unitComboBox.current(0) +unitComboBox.bind('<>', on_bind_event_device_unit_cb) +unitComboBox.grid( + column=1, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +required_frame_row_num += 1 + +def on_command_width_height_cb(): + update_device_unit_width_height() + +widthTextLabel = Label(requiredInputFrame, text='Width:') +widthTextLabel.grid( + column=0, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +widthSpinBox = Spinbox( + requiredInputFrame, + from_=0, + to=10000, + increment=0.1, + state='readonly', + textvariable=strvarScreenWidth, + command=on_command_width_height_cb, +) +widthSpinBox.grid( + column=1, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +required_frame_row_num += 1 + +heightTextLabel = Label(requiredInputFrame, text='Height:') +heightTextLabel.grid( + column=0, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +heightSpinBox = Spinbox( + requiredInputFrame, + from_=0, + to=10000, + increment=0.1, + state='readonly', + textvariable=strvarScreenHeight, + command=on_command_width_height_cb, +) +heightSpinBox.grid( + column=1, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +required_frame_row_num += 1 + +def on_bind_event_mode_cb(e=None): + conversionMode = mode_argument_map[modeComboBox.current()] + arg = ( + conversion_mode_arg_name + ' ' + + conversionMode + ) + add_or_update_one_cmd_arg(conversion_mode_arg_name, arg) + +modeTextLabel = Label(requiredInputFrame, text='Conversion Mode:') +modeTextLabel.grid( + column=0, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +modeComboBox = Combobox( + requiredInputFrame, + state='readonly', + textvariable=strvarConversionMode, +) +modeComboBox['values'] = list(mode_choice_map.values()) +modeComboBox.current(0) +modeComboBox.bind('<>', on_bind_event_mode_cb) +modeComboBox.grid( + column=1, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +# info frame +conversion_tab_left_part_row_num += 1 + +strvarOutputFilePath = StringVar() +strvarCmdArgs = StringVar() + +infoFrame = Labelframe(conversionTab, text='Related Info') +infoFrame.grid( + column=conversion_tab_left_part_column_num, + row=conversion_tab_left_part_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +def on_command_save_cb(): + with open(custom_preset_file_path, 'w') as preset_file: + dict_to_save = {} + for k, v in arg_var_map.items(): + dict_to_save[k] = [var.get() for var in v] + + json.dump(dict_to_save, preset_file) + +saveTextLabel = Label(infoFrame, text='Save Current Setting as Preset:') +saveTextLabel.grid(column=0, row=0, sticky=N+W, pady=0, padx=5) + +saveButton = Button(infoFrame, text='Save', command=on_command_save_cb) +saveButton.grid(column=1, row=0, sticky=N+W, pady=0, padx=5) + +outputTextLabel = Label(infoFrame, text='Output Pdf File Path:') +outputTextLabel.grid(column=0, row=1, sticky=N+W, pady=0, padx=5) + +outputPathEntry = Entry( + infoFrame, + state='readonly', + textvariable=strvarOutputFilePath, +) +outputPathEntry.grid(column=1, row=1, sticky=N+W, pady=0, padx=5) + +cmdArgTextLabel = Label(infoFrame, text='Command-line Options:') +cmdArgTextLabel.grid(column=0, row=2, sticky=N+W, pady=0, padx=5) + +def on_bind_event_cmd_args_cb(e=None): + update_cmd_arg_entry_strvar() + +cmdArgEntry = Entry( + infoFrame, + state='readonly', + textvariable=strvarCmdArgs, +) +cmdArgEntry.bind('', on_bind_event_cmd_args_cb) +cmdArgEntry.grid(column=1, row=2, sticky=N+W, pady=0, padx=5) + +# parameters +conversion_tab_left_part_row_num += 1 + +column_num_arg_name = '-col' # -col +resolution_multiplier_arg_name = '-dr' # -dr +crop_margin_arg_name = '-cbox' # -cbox[|u|-] +dpi_arg_name = '-dpi' # -dpi +page_num_arg_name = '-p' # -p + +isColumnNum = BooleanVar() +isResolutionMultipler = BooleanVar() +isCropMargin = BooleanVar() +isDPI = BooleanVar() + +strvarColumnNum = StringVar() +strvarResolutionMultiplier = StringVar() +strvarCropPageRange = StringVar() +strvarLeftMargin = StringVar() +strvarRightMargin = StringVar() +strvarTopMargin = StringVar() +strvarBottomMargin = StringVar() +strvarDPI = StringVar() +strvarPageNums = StringVar() + +paraFrame = Labelframe(conversionTab, text='Parameters') +paraFrame.grid( + column=conversion_tab_left_part_column_num, + row=conversion_tab_left_part_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num = 0 + +def on_command_column_num_cb(): + if isColumnNum.get(): + arg = ( + column_num_arg_name + ' ' + + strvarColumnNum.get().strip() + ) + add_or_update_one_cmd_arg(column_num_arg_name, arg) + else: + remove_one_cmd_arg(column_num_arg_name) + +colNumCheckButton = Checkbutton( + paraFrame, + text='Max Columns:', + variable=isColumnNum, + command=on_command_column_num_cb, +) +colNumCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +colNumSpinBox = Spinbox( + paraFrame, + from_=1, + to=10, + increment=1, + state='readonly', + textvariable=strvarColumnNum, + command=on_command_column_num_cb, +) +colNumSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +def on_command_resolution_multipler_cb(): + if isResolutionMultipler.get(): + arg = ( + resolution_multiplier_arg_name + ' ' + + strvarResolutionMultiplier.get().strip() + ) + add_or_update_one_cmd_arg(resolution_multiplier_arg_name, arg) + else: + remove_one_cmd_arg(resolution_multiplier_arg_name) + +resolutionCheckButton = Checkbutton( + paraFrame, + text='Document Resolution Factor:', + variable=isResolutionMultipler, + command=on_command_resolution_multipler_cb, +) +resolutionCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +resolutionSpinBox = Spinbox( + paraFrame, + from_=0.1, + to=10.0, + increment=0.1, + state='readonly', + textvariable=strvarResolutionMultiplier, + command=on_command_resolution_multipler_cb, +) +resolutionSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +def on_command_and_validate_crop_margin_cb(): + if (len(strvarCropPageRange.get().strip()) > 0 and + not check_page_nums(strvarCropPageRange.get().strip())): + remove_one_cmd_arg(crop_margin_arg_name) + strvarCropPageRange.set('') + + tkinter.messagebox.showerror( + message='Invalide Crop Page Range! Should be like 2-5e,3-7o,9-', + ) + + return FALSE + + if isCropMargin.get(): + page_range_arg = strvarCropPageRange.get().strip() + margin_args = [ + strvarLeftMargin.get(), + strvarTopMargin.get(), + strvarRightMargin.get(), + strvarBottomMargin.get(), + ] + arg = ( + # no space between -cbox and page range + crop_margin_arg_name + page_range_arg + ' ' + 'in,'.join(map(str.strip, margin_args)) + 'in' + ) + add_or_update_one_cmd_arg(crop_margin_arg_name, arg) + else: + remove_one_cmd_arg(crop_margin_arg_name) + +marginCheckButton = Checkbutton( + paraFrame, + text='Crop Margins (in):', + variable=isCropMargin, + command=on_command_and_validate_crop_margin_cb, +) +marginCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +cropPageRangeTextLabel = Label(paraFrame, text='Page Range:') +cropPageRangeTextLabel.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +cropPageRangeEntry = Entry( + paraFrame, + textvariable=strvarCropPageRange, + validate='focusout', + validatecommand=on_command_and_validate_crop_margin_cb, +) +cropPageRangeEntry.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +leftMarginTextLabel = Label(paraFrame, text='Left Margin:') +leftMarginTextLabel.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +leftMarginSpinBox = Spinbox( + paraFrame, + from_=0, + to=100, + increment=0.01, + state='readonly', + textvariable=strvarLeftMargin, + command=on_command_and_validate_crop_margin_cb, +) +leftMarginSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +rightMarginTextLabel = Label( + paraFrame, + text='Right Margin:', +) +rightMarginTextLabel.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +rightMarginSpinBox = Spinbox( + paraFrame, + from_=0, + to=100, + increment=0.01, + state='readonly', + textvariable=strvarRightMargin, + command=on_command_and_validate_crop_margin_cb, +) +rightMarginSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +topMarginTextLabel = Label(paraFrame, text='Top Margin:') +topMarginTextLabel.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +topMarginSpinBox = Spinbox( + paraFrame, + from_=0, + to=100, + increment=0.01, + state='readonly', + textvariable=strvarTopMargin, + command=on_command_and_validate_crop_margin_cb, +) +topMarginSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +bottomMarginTextLabel = Label(paraFrame, text='Bottom Margin:') +bottomMarginTextLabel.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +bottomMarginSpinBox = Spinbox( + paraFrame, + from_=0, + to=100, + increment=0.01, + state='readonly', + textvariable=strvarBottomMargin, + command=on_command_and_validate_crop_margin_cb, +) +bottomMarginSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +def on_command_dpi_cb(): + if isDPI.get(): + arg = dpi_arg_name + ' ' + strvarDPI.get().strip() + add_or_update_one_cmd_arg(dpi_arg_name, arg) + else: + remove_one_cmd_arg(dpi_arg_name) + +dpiCheckButton = Checkbutton( + paraFrame, + text='DPI:', + variable=isDPI, + command=on_command_dpi_cb, +) +dpiCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +dpiSpinBox = Spinbox( + paraFrame, + from_=0, + to=1000, + increment=1, + state='readonly', + textvariable=strvarDPI, + command=on_command_dpi_cb, +) +dpiSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +def validate_and_update_page_nums(): + if (len(strvarPageNums.get().strip()) > 0 and + not check_page_nums(strvarPageNums.get().strip())): + remove_one_cmd_arg(page_num_arg_name) + strvarPageNums.set('') + + tkinter.messagebox.showerror( + message='Invalide Page Argument! Should be like 2-5e,3-7o,9-', + ) + + return FALSE + + if len(strvarPageNums.get().strip()) > 0: + arg = page_num_arg_name + ' ' + strvarPageNums.get().strip() + add_or_update_one_cmd_arg(page_num_arg_name, arg) + else: + remove_one_cmd_arg(page_num_arg_name) + + return TRUE + +pageNumTextLabel = Label( + paraFrame, + text='Pages to Convert:', +) +pageNumTextLabel.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +def on_validate_page_nums_cb(): + validate_and_update_page_nums() + +pageNumEntry = Entry( + paraFrame, + textvariable=strvarPageNums, + validate='focusout', + validatecommand=on_validate_page_nums_cb, +) +pageNumEntry.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +fixed_font_size_arg_name = '-fs' # -fs 0/-fs [+] +ocr_arg_name = '-ocr' # -ocr-/-ocr t +ocr_cpu_arg_name = '-nt' # -nt -50/-nt +landscape_arg_name = '-ls' # -ls[-][pagelist] +linebreak_arg_name = '-ws' # -ws + +isFixedFontSize = BooleanVar() +isOCR = BooleanVar() +isLandscape = BooleanVar() +isLinebreak = BooleanVar() # -ws 0.01~10 + +strvarFixedFontSize = StringVar() +strvarOcrCpuPercentage = StringVar() +strvarLandscapePages = StringVar() # 1,3,5-10 +strvarLinebreakSpace = StringVar() + +# checkbox with value options +para_frame_row_num += 1 + +def on_command_fixed_font_size_cb(): + if isFixedFontSize.get(): + arg = ( + fixed_font_size_arg_name + ' ' + + strvarFixedFontSize.get().strip() + ) + add_or_update_one_cmd_arg( + fixed_font_size_arg_name, + arg, + ) + else: + remove_one_cmd_arg(fixed_font_size_arg_name) + +fontSizeCheckButton = Checkbutton( + paraFrame, + text='Fixed Output Font Size:', + variable=isFixedFontSize, + command=on_command_fixed_font_size_cb, +) +fontSizeCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +fontSizeSpinBox = Spinbox( + paraFrame, + from_=0, + to=100, + increment=1, + state='readonly', + textvariable=strvarFixedFontSize, + command=on_command_fixed_font_size_cb, +) +fontSizeSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +def on_command_ocr_and_cpu_cb(): + if isOCR.get(): + # ocr conflicts with native pdf + isNativePdf.set(FALSE) + remove_one_cmd_arg(native_pdf_arg_name) + + ocr_arg = ocr_arg_name + add_or_update_one_cmd_arg(ocr_arg_name, ocr_arg) + + # negtive integer means percentage + ocr_cpu_arg = ( + ocr_cpu_arg_name + '-' + + strvarOcrCpuPercentage.get().strip() + ) + add_or_update_one_cmd_arg( + ocr_cpu_arg_name, + ocr_cpu_arg, + ) + else: + remove_one_cmd_arg(ocr_arg_name) + remove_one_cmd_arg(ocr_cpu_arg_name) + +ocrCheckButton = Checkbutton( + paraFrame, + text='OCR (Tesseract) and CPU %:', + variable=isOCR, + command=on_command_ocr_and_cpu_cb, +) +ocrCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +ocrCpuSpinBox = Spinbox( + paraFrame, + from_=0, + to=100, + increment=1, + state='readonly', + textvariable=strvarOcrCpuPercentage, + command=on_command_ocr_and_cpu_cb, +) +ocrCpuSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +def on_command_and_validate_landscape_cb(): + if (len(strvarLandscapePages.get().strip()) > 0 and + not check_page_nums(strvarLandscapePages.get().strip())): + remove_one_cmd_arg(landscape_arg_name) + strvarLandscapePages.set('') + + tkinter.messagebox.showerror( + message='Invalide Landscape Page Argument!', + ) + + return FALSE + + if isLandscape.get(): + arg = '-ls' + if len(strvarLandscapePages.get().strip()) > 0: + # no space between -ls and page numbers + arg += strvarLandscapePages.get() + + add_or_update_one_cmd_arg(landscape_arg_name, arg.strip()) + else: + remove_one_cmd_arg(landscape_arg_name) + + return TRUE + +landscapeCheckButton = Checkbutton( + paraFrame, + text='Output in Landscape:', + variable=isLandscape, + command=on_command_and_validate_landscape_cb, +) +landscapeCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +landscapePageNumEntry = Entry( + paraFrame, + textvariable=strvarLandscapePages, + validate='focusout', + validatecommand=on_command_and_validate_landscape_cb, +) +landscapePageNumEntry.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +def on_command_line_break_cb(): + if isLinebreak.get(): + arg = ( + linebreak_arg_name + ' ' + + strvarLinebreakSpace.get().strip() + ) + add_or_update_one_cmd_arg(linebreak_arg_name, arg) + else: + remove_one_cmd_arg(linebreak_arg_name) + +lineBreakCheckButton = Checkbutton( + paraFrame, + text='Smart Line Breaks:', + variable=isLinebreak, + command=on_command_line_break_cb, +) +lineBreakCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +lineBreakSpinBox = Spinbox( + paraFrame, + from_=0.01, + to=2.00, + increment=0.01, + state='readonly', + textvariable=strvarLinebreakSpace, + command=on_command_line_break_cb, +) +lineBreakSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +# right side of conversion tab +conversion_tab_right_part_column_num = 1 +conversion_tab_right_part_row_num = -1 + +# options +conversion_tab_right_part_row_num += 1 + +auto_straignten_arg_name = '-as' # -as-/-as +break_page_avoid_overlap_arg_name = '-bp' # -bp-/-bp +color_output_arg_name = '-c' # -c-/-c +native_pdf_arg_name = '-n' # -n-/-n +right_to_left_arg_name = '-r' # -r-/-r +post_gs_arg_name = '-ppgs' # -ppgs-/-ppgs +marked_source_arg_name = '-sm' # -sm-/-sm +reflow_text_arg_name = '-wrap' # -wrap+/-wrap- +erase_vertical_line_arg_name = '-evl' # -evl 0/-evl 1 +erase_horizontal_line_arg_name = '-ehl' # -ehl 0/-ehl 1 +fast_preview_arg_name = '-rt' # -rt /-rt 0 +ign_small_defects_arg_name = '-de' # -de 1.0/-de 1.5 +auto_crop_arg_name = '-ac' # -ac-/-ac + +isAutoStraighten = BooleanVar() +isBreakPage = BooleanVar() +isColorOutput = BooleanVar() +isNativePdf = BooleanVar() +isRight2Left = BooleanVar() +isPostGs = BooleanVar() +isMarkedSrc = BooleanVar() +isReflowText = BooleanVar() +isEraseVerticalLine = BooleanVar() +isEraseHorizontalLine = BooleanVar() +isFastPreview = BooleanVar() +isAvoidOverlap = BooleanVar() +isIgnSmallDefects = BooleanVar() +isAutoCrop = BooleanVar() + +optionFrame = Labelframe( + conversionTab, + text='Options', +) +optionFrame.grid( + column=conversion_tab_right_part_column_num, + row=conversion_tab_right_part_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +option_frame_left_part_col_num = 0 +option_frame_row_num = 0 + +def on_command_auto_straighten_cb(): + if isAutoStraighten.get(): + arg = auto_straignten_arg_name + add_or_update_one_cmd_arg(auto_straignten_arg_name, arg) + else: + remove_one_cmd_arg(auto_straignten_arg_name) + +opt1 = Checkbutton( + optionFrame, + text='Autostraighten', + variable=isAutoStraighten, + command=on_command_auto_straighten_cb, +) +opt1.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + +def on_command_break_page_cb(): + if isBreakPage.get(): + # break page conflicts with avoid overlap since they are both -bp flag + isAvoidOverlap.set(FALSE) + remove_one_cmd_arg(break_page_avoid_overlap_arg_name) + + arg = break_page_avoid_overlap_arg_name + add_or_update_one_cmd_arg( + break_page_avoid_overlap_arg_name, + arg, + ) + else: + remove_one_cmd_arg(break_page_avoid_overlap_arg_name) + +opt2 = Checkbutton( + optionFrame, + text='Break After Each Source Page', + variable=isBreakPage, + command=on_command_break_page_cb, +) +opt2.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + +def on_command_color_output_cb(): + if isColorOutput.get(): + arg = color_output_arg_name + add_or_update_one_cmd_arg(color_output_arg_name, arg) + else: + remove_one_cmd_arg(color_output_arg_name) + +opt3 = Checkbutton( + optionFrame, + text='Color Output', + variable=isColorOutput, + command=on_command_color_output_cb, +) +opt3.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + +def on_command_native_pdf_cb(): + if isNativePdf.get(): + # native pdf conflicts with ocr and reflow text + isOCR.set(FALSE) + remove_one_cmd_arg(ocr_arg_name) + remove_one_cmd_arg(ocr_cpu_arg_name) + + isReflowText.set(FALSE) + remove_one_cmd_arg(reflow_text_arg_name) + + arg = native_pdf_arg_name + add_or_update_one_cmd_arg(native_pdf_arg_name, arg) + else: + remove_one_cmd_arg(native_pdf_arg_name) + +opt4 = Checkbutton( + optionFrame, + text='Native PDF Output', + variable=isNativePdf, + command=on_command_native_pdf_cb, +) +opt4.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + +def on_command_right_to_left_cb(): + if isRight2Left.get(): + arg = right_to_left_arg_name + add_or_update_one_cmd_arg(right_to_left_arg_name, arg) + else: + remove_one_cmd_arg(right_to_left_arg_name) + +opt5 = Checkbutton( + optionFrame, + text='Right-to-Left Text', + variable=isRight2Left, + command=on_command_right_to_left_cb, +) +opt5.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + +def on_command_post_gs_cb(): + if isPostGs.get(): + arg = post_gs_arg_name + add_or_update_one_cmd_arg(post_gs_arg_name, arg) + else: + remove_one_cmd_arg(post_gs_arg_name) + +opt6 = Checkbutton( + optionFrame, + text='Post Process w/GhostScript', + variable=isPostGs, + command=on_command_post_gs_cb, +) +opt6.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + +def on_command_marked_src_cb(): + if isMarkedSrc.get(): + arg = marked_source_arg_name + add_or_update_one_cmd_arg( + marked_source_arg_name, + arg, + ) + else: + remove_one_cmd_arg(marked_source_arg_name) + +opt7 = Checkbutton( + optionFrame, + text='Generate Marked-up Source', + variable=isMarkedSrc, + command=on_command_marked_src_cb, +) +opt7.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +option_frace_right_part_col_num = 1 +option_frame_row_num = 0 + +def on_command_reflow_text_cb(): + if isReflowText.get(): + # reflow text conflicts with native pdf + isNativePdf.set(FALSE) + remove_one_cmd_arg(native_pdf_arg_name) + + arg = reflow_text_arg_name + '+' + add_or_update_one_cmd_arg( + reflow_text_arg_name, + arg, + ) + else: + remove_one_cmd_arg(reflow_text_arg_name) + +opt8 = Checkbutton( + optionFrame, + text='Re-flow Text', + variable=isReflowText, + command=on_command_reflow_text_cb, +) +opt8.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + +def on_command_erase_vertical_line_cb(): + if isEraseVerticalLine.get(): + arg = erase_vertical_line_arg_name + ' 1' + add_or_update_one_cmd_arg( + erase_vertical_line_arg_name, + arg, + ) + else: + remove_one_cmd_arg(erase_vertical_line_arg_name) + +opt9 = Checkbutton( + optionFrame, + text='Erase Vertical Lines', + variable=isEraseVerticalLine, + command=on_command_erase_vertical_line_cb, +) +opt9.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + +def on_command_erase_horizontal_line_cb(): + if isEraseHorizontalLine.get(): + arg = erase_horizontal_line_arg_name + ' 1' + add_or_update_one_cmd_arg( + erase_horizontal_line_arg_name, + arg, + ) + else: + remove_one_cmd_arg(erase_horizontal_line_arg_name) + +opt14 = Checkbutton( + optionFrame, + text='Erase Horizontal Lines', + variable=isEraseHorizontalLine, + command=on_command_erase_horizontal_line_cb, +) +opt14.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + +def on_command_fast_preview_cb(): + if isFastPreview.get(): + arg = fast_preview_arg_name + ' 0' + add_or_update_one_cmd_arg(fast_preview_arg_name, arg) + else: + remove_one_cmd_arg(fast_preview_arg_name) + +opt10 = Checkbutton( + optionFrame, + text='Fast Preview', + variable=isFastPreview, + command=on_command_fast_preview_cb, +) +opt10.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + +def on_command_avoid_text_selection_overlap_cb(): + if isAvoidOverlap.get(): + # avoid overlap conflicts with break page since they are both -bp flag + isBreakPage.set(FALSE) + remove_one_cmd_arg(break_page_avoid_overlap_arg_name) + + arg = break_page_avoid_overlap_arg_name + ' m' + add_or_update_one_cmd_arg( + break_page_avoid_overlap_arg_name, + arg, + ) + else: + remove_one_cmd_arg(break_page_avoid_overlap_arg_name) + +opt11 = Checkbutton( + optionFrame, + text='Avoid Text Selection Overlap', + variable=isAvoidOverlap, + command=on_command_avoid_text_selection_overlap_cb, +) +opt11.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + +def on_command_ign_small_defect_cb(): + if isIgnSmallDefects.get(): + arg = ( + ign_small_defects_arg_name + ' 1.5' + ) + add_or_update_one_cmd_arg( + ign_small_defects_arg_name, + arg, + ) + else: + remove_one_cmd_arg(ign_small_defects_arg_name) + +opt12 = Checkbutton( + optionFrame, + text='Ignore Small Defects', + variable=isIgnSmallDefects, + command=on_command_ign_small_defect_cb, +) +opt12.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + +def on_command_auto_crop_cb(): + if isAutoCrop.get(): + arg = auto_crop_arg_name + add_or_update_one_cmd_arg(auto_crop_arg_name, arg) + else: + remove_one_cmd_arg(auto_crop_arg_name) + +opt13 = Checkbutton( + optionFrame, + text='Auto-Crop', + variable=isAutoCrop, + command=on_command_auto_crop_cb, +) +opt13.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + +# preview frame +conversion_tab_right_part_row_num += 1 + +preview_output_arg_name = '-bmp' +preview_image_path = './k2pdfopt_out.png' +current_preview_page_index = 1 + +# global variable to hold opened preview image to prevent gc collecting it +preview_img = None +canvas_image_tag = None + + +strvarCurrentPreviewPageNum = StringVar() + +def remove_preview_img_and_clear_canvas(): + global canvas_image_tag + global strvarCurrentPreviewPageNum + + if os.path.exists(preview_image_path): + + os.remove(preview_image_path) + + previewImageCanvas.delete(ALL) + canvas_image_tag = None + +def load_image_to_canvas(photo_img, canvas): + load_image_to_canvas(preview_img, previewImageCanvas) + +def load_preview_image(img_path, preview_page_index): + # PhotoImage must be global var to prevent gc collect it + global preview_img + global previewImageCanvas + + if os.path.exists(img_path): + preview_img = PhotoImage(file=img_path) + + canvas_image_tag = previewImageCanvas.create_image( + (0, 0), + anchor=NW, + image=preview_img, + tags='preview', + ) + + (left_pos, top_pos, right_pos, bottom_pos) = ( + 0, + 0, + preview_img.width(), + preview_img.height(), + ) + previewImageCanvas.config( + scrollregion=(left_pos, top_pos, right_pos, bottom_pos), + ) + # canvas.scale('preview', 0, 0, 0.1, 0.1) + strvarCurrentPreviewPageNum.set('Page: ' + str(preview_page_index)) + else: + strvarCurrentPreviewPageNum.set('No Page ' + str(preview_page_index)) + +def generate_one_preview_image(preview_page_index): + global background_future + + if not check_pdf_conversion_done(): + return + + if not os.path.exists(strvarFilePath.get().strip()): + tkinter.messagebox.showerror( + message=( + "Failed to Find Input PDF File to convert for Preview: %s" + % + strvarFilePath.get().strip() + ), + ) + return + + remove_preview_img_and_clear_canvas() + + (base_path, file_ext) = os.path.splitext(strvarFilePath.get().strip()) + + output_arg = ' '.join([preview_output_arg_name, str(preview_page_index)]) + + background_future = convert_pdf_file(output_arg) + + strvarCurrentPreviewPageNum.set('Preview Generating...') + + def preview_image_future_cb(bgf): + load_preview_image(preview_image_path, preview_page_index) + log_string( + "Preview generation for page %d finished" % + preview_page_index + ) + + background_future.add_done_callback(preview_image_future_cb) + +previewFrame = Labelframe(conversionTab, text='Preview & Convert') +previewFrame.grid( + column=conversion_tab_right_part_column_num, + row=conversion_tab_right_part_row_num, + rowspan=3, + sticky=N+S+E+W, + pady=0, + padx=5, +) + +preview_frame_row_num = 0 + +def on_command_restore_default_cb(): + restore_default_values() + +resetButton = Button( + previewFrame, + text='Reset Default', + command=on_command_restore_default_cb, +) +resetButton.grid( + column=0, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +def on_command_abort_conversion_cb(): + global background_future + global background_process + + if background_future is not None: + background_future.cancel() + + if (background_process is not None and + background_process.returncode is None): + background_process.terminate() + +cancelButton = Button( + previewFrame, + text='Abort', + command=on_command_abort_conversion_cb, +) +cancelButton.grid( + column=1, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +def on_command_convert_pdf_cb(): + if not check_pdf_conversion_done(): + return + + global background_future + + pdf_output_arg = output_path_arg_name + ' %s' + output_pdf_suffix + background_future = convert_pdf_file(pdf_output_arg) + +convertButton = Button( + previewFrame, + text='Convert', + command=on_command_convert_pdf_cb, +) +convertButton.grid( + column=2, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +preview_frame_row_num += 1 + +currentPreviewPageNumEntry = Entry( + previewFrame, + state='readonly', + textvariable=strvarCurrentPreviewPageNum) +currentPreviewPageNumEntry.grid( + column=0, + row=preview_frame_row_num, + columnspan=2, + sticky=N+W, + pady=0, + padx=5, +) + +def on_command_ten_page_up_cb(): + global current_preview_page_index + current_preview_page_index -= 10 + if current_preview_page_index < 1: + current_preview_page_index = 1 + generate_one_preview_image(current_preview_page_index) + +previewButton = Button( + previewFrame, + text='Preview', + command=on_command_ten_page_up_cb, +) +previewButton.grid( + column=2, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +preview_frame_column_num = 0 +preview_frame_row_num += 1 + +firstButton = Button( + previewFrame, + text='<<', + command=on_command_ten_page_up_cb, +) +firstButton.grid( + column=preview_frame_column_num, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +preview_frame_column_num += 1 + +def on_command_page_up_cb(): + global current_preview_page_index + if current_preview_page_index > 1: + current_preview_page_index -= 1 + generate_one_preview_image(current_preview_page_index) + +previousButton = Button( + previewFrame, + text='<', + command=on_command_page_up_cb, +) +previousButton.grid( + column=preview_frame_column_num, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +preview_frame_column_num += 1 + +def on_command_page_down_cb(): + global current_preview_page_index + current_preview_page_index += 1 + generate_one_preview_image(current_preview_page_index) + +nextButton = Button( + previewFrame, + text='>', + command=on_command_page_down_cb, +) +nextButton.grid( + column=preview_frame_column_num, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +preview_frame_column_num += 1 + +def on_command_ten_page_down_cb(): + global current_preview_page_index + current_preview_page_index += 10 + generate_one_preview_image(current_preview_page_index) + +lastButton = Button( + previewFrame, + text='>>', + command=on_command_ten_page_down_cb, +) +lastButton.grid( + column=preview_frame_column_num, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +preview_frame_column_num += 1 + +preview_frame_row_num += 1 + +xScrollBar = Scrollbar(previewFrame, orient=HORIZONTAL) +xScrollBar.grid( + column=0, + row=preview_frame_row_num+1, + columnspan=preview_frame_column_num, + sticky=E+W, +) + +yScrollBar = Scrollbar(previewFrame) +yScrollBar.grid( + column=preview_frame_column_num, + row=preview_frame_row_num, + sticky=N+S, +) + +previewImageCanvas = Canvas( + previewFrame, + bd=0, + xscrollcommand=xScrollBar.set, + yscrollcommand=yScrollBar.set, +) +previewImageCanvas.grid( + column=0, + row=preview_frame_row_num, + columnspan=preview_frame_column_num, + sticky=N+S+E+W, +) + +xScrollBar.config(command=previewImageCanvas.xview) +yScrollBar.config(command=previewImageCanvas.yview) + +conversionTab.columnconfigure( + conversion_tab_right_part_column_num, + weight=1, +) +conversionTab.rowconfigure( + conversion_tab_right_part_row_num, + weight=1, +) +previewFrame.columnconfigure(0, weight=1) +previewFrame.rowconfigure(preview_frame_row_num, weight=1) + +def yscroll_canvas(event): + previewImageCanvas.yview_scroll(-1 * event.delta, 'units') + +def xscroll_canvas(event): + previewImageCanvas.xview_scroll(-1 * event.delta, 'units') + +previewImageCanvas.bind('', yscroll_canvas) +previewImageCanvas.bind("", xscroll_canvas) + +preview_frame_row_num += 1 + +# collect all vars + +bool_var_list = [ + isColumnNum, + isResolutionMultipler, + isCropMargin, + isDPI, + isFixedFontSize, + isOCR, + isLandscape, + isLinebreak, + + isAutoStraighten, + isBreakPage, + isColorOutput, + isNativePdf, + isRight2Left, + isPostGs, + isMarkedSrc, + isReflowText, + isEraseVerticalLine, + isEraseHorizontalLine, + isFastPreview, + isAvoidOverlap, + isIgnSmallDefects, + isAutoCrop, +] + +string_var_list = [ + strvarFilePath, + strvarDevice, + strvarConversionMode, + strvarScreenUnit, + strvarScreenWidth, + strvarScreenHeight, + + strvarOutputFilePath, + strvarCmdArgs, + + strvarColumnNum, + strvarResolutionMultiplier, + strvarCropPageRange, + strvarLeftMargin, + strvarRightMargin, + strvarTopMargin, + strvarBottomMargin, + strvarDPI, + strvarPageNums, + + strvarFixedFontSize, + strvarOcrCpuPercentage, + strvarLandscapePages, + strvarLinebreakSpace, + + strvarCurrentPreviewPageNum, +] + +combo_box_list = [ + deviceComboBox, + modeComboBox, + unitComboBox, +] + +entry_list = [ + inputPathEntry, + outputPathEntry, + cmdArgEntry, + pageNumEntry, + landscapePageNumEntry, + currentPreviewPageNumEntry, +] + +default_var_map = { + device_arg_name: ['Kindle 1-5'], + screen_unit_prefix: ['Pixels'], + width_arg_name: ['560'], + height_arg_name: ['735'], + conversion_mode_arg_name: ['Default'], + output_path_arg_name: [''], + + column_num_arg_name: [FALSE, '2'], + resolution_multiplier_arg_name: [FALSE, '1.0'], + crop_margin_arg_name: [ + FALSE, + '', + '0.00', + '0.00', + '0.00', + '0.00', + ], + dpi_arg_name: [FALSE, '167'], + page_num_arg_name: [''], + fixed_font_size_arg_name: [FALSE, '12'], + ocr_arg_name: [FALSE, '50'], + ocr_cpu_arg_name: [FALSE, '50'], + landscape_arg_name: [FALSE, ''], + linebreak_arg_name: [TRUE, '0.200'], + + auto_straignten_arg_name: [FALSE], + break_page_avoid_overlap_arg_name: [FALSE, FALSE], + color_output_arg_name: [FALSE], + native_pdf_arg_name: [FALSE], + right_to_left_arg_name: [FALSE], + post_gs_arg_name: [FALSE], + marked_source_arg_name: [FALSE], + reflow_text_arg_name: [TRUE], + erase_vertical_line_arg_name: [FALSE], + erase_horizontal_line_arg_name: [FALSE], + fast_preview_arg_name: [TRUE], + ign_small_defects_arg_name: [FALSE], + auto_crop_arg_name: [FALSE], + + preview_output_arg_name: [] +} + +arg_var_map = { + device_arg_name: [strvarDevice], + screen_unit_prefix: [strvarScreenUnit], + width_arg_name: [strvarScreenWidth], + height_arg_name: [strvarScreenHeight], + conversion_mode_arg_name: [strvarConversionMode], + output_path_arg_name: [strvarOutputFilePath], + + column_num_arg_name: [ + isColumnNum, + strvarColumnNum, + ], + resolution_multiplier_arg_name: [ + isResolutionMultipler, + strvarResolutionMultiplier, + ], + crop_margin_arg_name: [ + isCropMargin, + strvarCropPageRange, + strvarLeftMargin, + strvarTopMargin, + strvarRightMargin, + strvarBottomMargin, + ], + dpi_arg_name: [ + isDPI, + strvarDPI, + ], + page_num_arg_name: [ + strvarPageNums, + ], + + fixed_font_size_arg_name: [ + isFixedFontSize, + strvarFixedFontSize, + ], + ocr_arg_name: [ + isOCR, + strvarOcrCpuPercentage, + ], + ocr_cpu_arg_name: [ + isOCR, + strvarOcrCpuPercentage, + ], + landscape_arg_name: [ + isLandscape, + strvarLandscapePages, + ], + linebreak_arg_name: [ + isLinebreak, + strvarLinebreakSpace, + ], + + auto_straignten_arg_name: [isAutoStraighten], + break_page_avoid_overlap_arg_name: [isBreakPage, isAvoidOverlap], + color_output_arg_name: [isColorOutput], + native_pdf_arg_name: [isNativePdf], + right_to_left_arg_name: [isRight2Left], + post_gs_arg_name: [isPostGs], + marked_source_arg_name: [isMarkedSrc], + reflow_text_arg_name: [isReflowText], + erase_vertical_line_arg_name: [isEraseVerticalLine], + erase_horizontal_line_arg_name: [isEraseHorizontalLine], + fast_preview_arg_name: [isFastPreview], + # break_page_avoid_overlap_arg_name: [] + ign_small_defects_arg_name: [isIgnSmallDefects], + auto_crop_arg_name: [isAutoCrop], + preview_output_arg_name: [] +} + +arg_cb_map = { + device_arg_name: on_bind_event_device_unit_cb, + width_arg_name: on_command_width_height_cb, + height_arg_name: on_command_width_height_cb, + conversion_mode_arg_name: on_bind_event_mode_cb, + output_path_arg_name: None, + + column_num_arg_name: on_command_column_num_cb, + resolution_multiplier_arg_name: on_command_resolution_multipler_cb, + crop_margin_arg_name: on_command_and_validate_crop_margin_cb, + dpi_arg_name: on_command_dpi_cb, + page_num_arg_name: on_validate_page_nums_cb, + + fixed_font_size_arg_name: on_command_fixed_font_size_cb, + ocr_arg_name: on_command_ocr_and_cpu_cb, + ocr_cpu_arg_name: on_command_ocr_and_cpu_cb, + landscape_arg_name: on_command_and_validate_landscape_cb, + linebreak_arg_name: on_command_line_break_cb, + + auto_straignten_arg_name: on_command_auto_straighten_cb, + break_page_avoid_overlap_arg_name: on_command_break_page_cb, + color_output_arg_name: on_command_color_output_cb, + native_pdf_arg_name: on_command_native_pdf_cb, + right_to_left_arg_name: on_command_right_to_left_cb, + post_gs_arg_name: on_command_post_gs_cb, + marked_source_arg_name: on_command_marked_src_cb, + reflow_text_arg_name: on_command_reflow_text_cb, + erase_vertical_line_arg_name: on_command_erase_vertical_line_cb, + erase_horizontal_line_arg_name: on_command_erase_horizontal_line_cb, + fast_preview_arg_name: on_command_fast_preview_cb, + ign_small_defects_arg_name: on_command_ign_small_defect_cb, + auto_crop_arg_name: on_command_auto_crop_cb, + + preview_output_arg_name: None +} + +# k2pdfopt stdout + +stdoutFrame = Labelframe(logTab, text='k2pdfopt STDOUT:') +stdoutFrame.pack(expand=1, fill='both') + +def on_command_clear_log_cb(): + clear_logs() + +clearButton = Button( + stdoutFrame, + text='Clear', + command=on_command_clear_log_cb) +clearButton.grid( + column=0, + row=0, + sticky=N+W, + pady=0, + padx=5, +) + +stdoutText = scrltxt.ScrolledText( + stdoutFrame, + state=DISABLED, wrap='word', +) +stdoutText.grid(column=0, row=1, sticky=N+S+E+W) +stdoutFrame.columnconfigure(0, weight=1) +stdoutFrame.rowconfigure(1, weight=1) +# stdoutText.pack(expand=1, fill='both') + +# initialization + +def initialize(): + check_k2pdfopt_path_exists() + + if not load_custom_preset(): + restore_default_values() + + pwd = os.getcwd() + log_string('Current directory: ' + pwd) + +initialize() + +# start TclTk loop + +root.mainloop() diff --git a/rebook_2.py b/rebook_2.py new file mode 100644 index 0000000..4446da4 --- /dev/null +++ b/rebook_2.py @@ -0,0 +1,2356 @@ +#!/usr/bin/python + +from threading import Thread +from tkinter import * +from tkinter.ttk import * +import asyncio +import glob +import json +import os +import subprocess as sub +import tkinter.filedialog +import tkinter.messagebox +import tkinter.scrolledtext as scrltxt +import tkinter.font as tkfont + + +def check_page_nums(input_pages): + page_num_list = re.split(',|-|o|e', input_pages) + + for page_num in page_num_list: + if len(page_num) > 0 and not page_num.isdigit(): + return FALSE + return TRUE + +# GUI construction + +root = Tk() + +screen_width = root.winfo_screenwidth() +screen_height = root.winfo_screenheight() + +# root.resizable(FALSE, FALSE) + +root.title('rebook') + +# main tab + +baseTab = Notebook(root) + +conversionTab = Frame(baseTab) +baseTab.add(conversionTab, text='Conversion') + +logTab = Frame(baseTab) +baseTab.add(logTab, text='Log') + +baseTab.pack(expand=1, fill='both') + +# ############################################################################################### # +# MENU +# ############################################################################################### # + +menu_bar = Menu(root) +root['menu'] = menu_bar + +def on_command_about_box_cb(): + theMessage = \ + '''rebook + +TclTk GUI for k2pdfopt by Pu Wang + +The source code can be found at: +http://github.com/pwang7/rebook/rebook.py''' + + tkinter.messagebox.showinfo(message=theMessage) + +menu_file = Menu(menu_bar) +menu_bar.add_cascade(menu=menu_file, label='File') +menu_file.add_command(label='About', command=on_command_about_box_cb) + +# root.createcommand('tkAboutDialog', on_command_about_box_cb) +# root.createcommand('::tk::mac::ShowHelp', on_command_about_box_cb) + +# global variables and functions + +k2pdfopt_path = './k2pdfopt' +custom_preset_file_path = 'rebook_preset.json' + + +def check_k2pdfopt_path_exists(): + if not os.path.exists(k2pdfopt_path): + tkinter.messagebox.showerror( + message='Failed to find k2pdfopt, ' + + 'please put it under the same directory ' + + 'as rebook and then restart.' + ) + quit() + +k2pdfopt_cmd_args = {} + + +def update_cmd_arg_entry_strvar(): + global strvarCmdArgs + strvarCmdArgs.set(generate_cmd_arg_str()) + + +def add_or_update_one_cmd_arg(arg_key, arg_value): + global k2pdfopt_cmd_args + k2pdfopt_cmd_args[arg_key] = arg_value + + +def remove_one_cmd_arg(arg_key): + global k2pdfopt_cmd_args + previous = k2pdfopt_cmd_args.pop(arg_key, None) + return previous + + +def load_custom_preset(): + global strvarOutputFilePath + + if os.path.exists(custom_preset_file_path): + with open(custom_preset_file_path) as preset_file: + dict_to_load = json.load(preset_file) + + if dict_to_load: + log_string('Load Preset: ' + str(dict_to_load)) + + initialize_vars(dict_to_load) + + return TRUE + + return FALSE + + +def log_string(str_line): + global stdoutText + + log_content = str_line.strip() + if len(log_content) > 0: + stdoutText.config(state=NORMAL) + + print('=== ' + log_content) # TODO: remove print + + stdoutText.insert(END, log_content + '\n') + stdoutText.config(state=DISABLED) + + +def clear_logs(): + stdoutText.config(state=NORMAL) + stdoutText.delete(1.0, END) + stdoutText.config(state=DISABLED) + + +def initialize_vars(dict_vars): + global k2pdfopt_cmd_args + + for k, v in dict_vars.items(): + for i in range(len(v)): + arg_var_map[k][i].set(v[i]) + + k2pdfopt_cmd_args = {} + + for cb_func in arg_cb_map.values(): + if cb_func is not None: + cb_func() + # must be after loading preset values + update_cmd_arg_entry_strvar() + + +def restore_default_values(): + clear_logs() + + remove_preview_img_and_clear_canvas() + + for sv in string_var_list: + sv.set('') + + for bv in bool_var_list: + bv.set(FALSE) + + for b in combo_box_list: + b.current(0) + + initialize_vars(default_var_map) + + +def start_loop(loop): + asyncio.set_event_loop(loop) + loop.run_forever() + +thread_loop = asyncio.get_event_loop() +run_loop_thread = Thread( + target=start_loop, + args=(thread_loop,), + daemon=TRUE, +) +run_loop_thread.start() + +background_process = None +background_future = None + +device_argument_map = { + 0: 'k2', + 1: 'dx', + 2: 'kpw', + 3: 'kp2', + 4: 'kp3', + 5: 'kv', + 6: 'ko2', + 7: 'pb2', + 8: 'nookst', + 9: 'kbt', + 10: 'kbg', + 11: 'kghd', + 12: 'kghdfs', + 13: 'kbm', + 14: 'kba', + 15: 'kbhd', + 16: 'kbh2o', + 17: 'kbh2ofs', + 18: 'kao', + 19: 'nex7', + 20: None, + } + +device_choice_map = { + 0: 'Kindle 1-5', + 1: 'Kindle DX', + 2: 'Kindle Paperwhite', + 3: 'Kindle Paperwhite 2', + 4: 'Kindle Paperwhite 3', + 5: 'Kindle Voyage/PW3+/Oasis', + 6: 'Kindle Oasis 2', + 7: 'Pocketbook Basic 2', + 8: 'Nook Simple Touch', + 9: 'Kobo Touch', + 10: 'Kobo Glo', + 11: 'Kobo Glo HD', + 12: 'Kobo Glo HD Full Screen', + 13: 'Kobo Mini', + 14: 'Kobo Aura', + 15: 'Kobo Aura HD', + 16: 'Kobo H2O', + 17: 'Kobo H2O Full Screen', + 18: 'Kobo Aura One', + 19: 'Nexus 7', + 20: 'Other (specify width & height)', + } + +mode_argument_map = { + 0: 'def', + 1: 'copy', + 2: 'fp', + 3: 'fw', + 4: '2col', + 5: 'tm', + 6: 'crop', + 7: 'concat', + } + +mode_choice_map = { + 0: 'Default', + 1: 'Copy', + 2: 'Fit Page', + 3: 'Fit Width', + 4: '2 Columns', + 5: 'Trim Margins', + 6: 'Crop', + 7: 'Concat', + } + +unit_argument_map = { + 0: 'in', + 1: 'cm', + 2: 's', + 3: 't', + 4: 'p', + 5: 'x', + } + +unit_choice_map = { + 0: 'Inches', + 1: 'Centimeters', + 2: 'Source Page Size', + 3: 'Trimmed Source Region Size', + 4: 'Pixels', + 5: 'Relative to the OCR Text Layer', + } + + +def generate_cmd_arg_str(): + must_have_args = '-a- -ui- -x' + + device_arg = k2pdfopt_cmd_args.pop(device_arg_name, None) + if device_arg is None: + width_arg = k2pdfopt_cmd_args.pop(width_arg_name) + height_arg = k2pdfopt_cmd_args.pop(height_arg_name) + + mode_arg = k2pdfopt_cmd_args.pop(conversion_mode_arg_name) + + arg_list = [mode_arg] + list(k2pdfopt_cmd_args.values()) + + k2pdfopt_cmd_args[conversion_mode_arg_name] = mode_arg + if device_arg is not None: + arg_list.append(device_arg) + + k2pdfopt_cmd_args[device_arg_name] = device_arg + else: + arg_list.append(width_arg) + arg_list.append(height_arg) + + k2pdfopt_cmd_args[width_arg_name] = width_arg + k2pdfopt_cmd_args[height_arg_name] = height_arg + + arg_list.append(must_have_args) + + log_string('Generate Argument List: ' + str(arg_list)) + + cmd_arg_str = ' '.join(arg_list) + + return cmd_arg_str + + +def convert_pdf_file(output_arg): + check_k2pdfopt_path_exists() + + async def async_run_cmd_and_log(exec_cmd): + global background_process + + executed = exec_cmd.strip() + + def log_bytes(log_btyes): + log_string(log_btyes.decode('utf-8')) + + log_string(executed) + + p = await asyncio.create_subprocess_shell( + executed, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + background_process = p + + while True: + line = await p.stdout.readline() + + log_bytes(line) + + if not line: + break + if line == '' and p.returncode is not None: + break + + input_pdf_path = strvarFilePath.get().strip() + if ' ' in input_pdf_path: + # in case the file name contains space + input_pdf_path = '\"' + input_pdf_path + '\"' + + executed = ' '.join([ + k2pdfopt_path, + input_pdf_path, + output_arg, + generate_cmd_arg_str(), + ]) + future = asyncio.run_coroutine_threadsafe( + async_run_cmd_and_log(executed), + thread_loop, + ) + return future + + +def check_pdf_conversion_done(): + if (background_future is None) or (background_future.done()): + if ((background_process is None) or + (background_process.returncode is not None)): + return TRUE + + tkinter.messagebox.showerror( + message='Background Conversion Not Finished Yet! Please Wait.', + ) + return FALSE + +# conversion tab +conversion_tab_left_part_column_num = 0 +conversion_tab_left_part_row_num = -1 + + +# ############################################################################################### # +# REQUIRED INPUTS +# ############################################################################################### # +conversion_tab_left_part_row_num += 1 + +device_arg_name = '-dev' # -dev +width_arg_name = '-w' # -w [in|cm|s|t|p] +height_arg_name = '-h' # -h [in|cm|s|t|p|x] +conversion_mode_arg_name = '-mode' # -mode +output_path_arg_name = '-o' # -o +output_pdf_suffix = '-output.pdf' +screen_unit_prefix = '-screen_unit' + +strvarFilePath = StringVar() +strvarDevice = StringVar() +strvarConversionMode = StringVar() +strvarScreenUnit = StringVar() +strvarScreenWidth = StringVar() +strvarScreenHeight = StringVar() + +# fontStyle = tkfont.Font(family="Lucida Grande", size=20) +# style = Style() +# # creating the theme with the +# style.theme_create('style_class', + +# # getting the settings +# settings={ + +# # getting through the Labelframe +# # widget +# 'TLabelframe': { + +# # configure the changes +# 'configure': { +# 'background': 'green' +# } +# }, + +# # getting through the Labelframe's +# # label widget +# 'TLabelframe.Label': { +# 'configure': { +# 'background': 'green' +# } +# } +# } +# ) +# style.theme_use('style_class') +required_input_frame = Labelframe(conversionTab, text='Required Inputs') +required_input_frame.grid( + column=conversion_tab_left_part_column_num, + row=conversion_tab_left_part_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +required_frame_row_num = 0 + +inputPathEntry = Entry( + required_input_frame, + state='readonly', + textvariable=strvarFilePath, +) +inputPathEntry.grid( + column=0, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + + +def on_command_open_pdf_file_cb(): + supported_formats = [('PDF files', '*.pdf'), ('DJVU files', '*.djvu')] + + filename = tkinter.filedialog.askopenfilename( + parent=root, + filetypes=supported_formats, + title='Select your file', + ) + + if filename is not None and len(filename.strip()) > 0: + strvarFilePath.set(filename) + (base_path, file_ext) = os.path.splitext(filename) + strvarOutputFilePath.set(base_path + output_pdf_suffix) + +openButton = Button( + required_input_frame, + text='Choose a File', + command=on_command_open_pdf_file_cb, +) +openButton.grid( + column=1, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +required_frame_row_num += 1 + +deviceText = Label(required_input_frame, text='Device') +deviceText.grid( + column=0, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + + +def update_device_unit_width_height(): + if deviceComboBox.current() != 20: # non-other type + deviceType = device_argument_map[deviceComboBox.current()] + arg = device_arg_name + ' ' + deviceType + add_or_update_one_cmd_arg(device_arg_name, arg) + + remove_one_cmd_arg(width_arg_name) + remove_one_cmd_arg(height_arg_name) + else: + screen_unit = unit_argument_map[unitComboBox.current()] + + width_arg = ( + width_arg_name + ' ' + + strvarScreenWidth.get().strip() + screen_unit + ) + add_or_update_one_cmd_arg(width_arg_name, width_arg) + + height_arg = ( + height_arg_name + ' ' + + strvarScreenHeight.get().strip() + screen_unit + ) + add_or_update_one_cmd_arg(height_arg_name, height_arg) + + remove_one_cmd_arg(device_arg_name) + + +def on_bind_event_device_unit_cb(e=None): + update_device_unit_width_height() + +deviceComboBox = Combobox( + required_input_frame, + state='readonly', + textvariable=strvarDevice, +) +deviceComboBox['values'] = list(device_choice_map.values()) +deviceComboBox.current(0) +deviceComboBox.bind('<>', on_bind_event_device_unit_cb) +deviceComboBox.grid( + column=1, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +required_frame_row_num += 1 + +unitTextLabel = Label(required_input_frame, text='Unit') +unitTextLabel.grid( + column=0, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +unitComboBox = Combobox( + required_input_frame, + state='readonly', + textvariable=strvarScreenUnit, +) +unitComboBox['values'] = list(unit_choice_map.values()) +unitComboBox.current(0) +unitComboBox.bind('<>', on_bind_event_device_unit_cb) +unitComboBox.grid( + column=1, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +required_frame_row_num += 1 + + +def on_command_width_height_cb(): + update_device_unit_width_height() + +widthTextLabel = Label(required_input_frame, text='Width') +widthTextLabel.grid( + column=0, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +widthSpinBox = Spinbox( + required_input_frame, + from_=0, + to=10000, + increment=0.1, + # state='readonly', + textvariable=strvarScreenWidth, + command=on_command_width_height_cb, +) +widthSpinBox.grid( + column=1, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +required_frame_row_num += 1 + +heightTextLabel = Label(required_input_frame, text='Height') +heightTextLabel.grid( + column=0, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +heightSpinBox = Spinbox( + required_input_frame, + from_=0, + to=10000, + increment=0.1, + # state='readonly', + textvariable=strvarScreenHeight, + command=on_command_width_height_cb, +) +heightSpinBox.grid( + column=1, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +required_frame_row_num += 1 + + +def on_bind_event_mode_cb(e=None): + conversionMode = mode_argument_map[modeComboBox.current()] + arg = ( + conversion_mode_arg_name + ' ' + + conversionMode + ) + add_or_update_one_cmd_arg(conversion_mode_arg_name, arg) + +modeTextLabel = Label(required_input_frame, text='Conversion Mode') +modeTextLabel.grid( + column=0, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +modeComboBox = Combobox( + required_input_frame, + state='readonly', + textvariable=strvarConversionMode, +) +modeComboBox['values'] = list(mode_choice_map.values()) +modeComboBox.current(0) +modeComboBox.bind('<>', on_bind_event_mode_cb) +modeComboBox.grid( + column=1, + row=required_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +# info frame +conversion_tab_left_part_row_num += 1 + +strvarOutputFilePath = StringVar() +strvarCmdArgs = StringVar() + +infoFrame = Labelframe(conversionTab, text='Related Info') +infoFrame.grid( + column=conversion_tab_left_part_column_num, + row=conversion_tab_left_part_row_num, + sticky=N+W, + pady=0, + padx=5, +) + + +def on_command_save_cb(): + with open(custom_preset_file_path, 'w') as preset_file: + dict_to_save = {} + for k, v in arg_var_map.items(): + dict_to_save[k] = [var.get() for var in v] + + json.dump(dict_to_save, preset_file) + +saveTextLabel = Label(infoFrame, text='Save Current Setting as Preset') +saveTextLabel.grid(column=0, row=0, sticky=N+W, pady=0, padx=5) + +saveButton = Button(infoFrame, text='Save', command=on_command_save_cb) +saveButton.grid(column=1, row=0, sticky=N+W, pady=0, padx=5) + +outputTextLabel = Label(infoFrame, text='Output Pdf File Path') +outputTextLabel.grid(column=0, row=1, sticky=N+W, pady=0, padx=5) + +outputPathEntry = Entry( + infoFrame, + state='readonly', + textvariable=strvarOutputFilePath, +) +outputPathEntry.grid(column=1, row=1, sticky=N+W, pady=0, padx=5) + +cmdArgTextLabel = Label(infoFrame, text='Command-line Options') +cmdArgTextLabel.grid(column=0, row=2, sticky=N+W, pady=0, padx=5) + + +def on_bind_event_cmd_args_cb(e=None): + update_cmd_arg_entry_strvar() + +cmdArgEntry = Entry( + infoFrame, + state='readonly', + textvariable=strvarCmdArgs, +) +cmdArgEntry.bind('', on_bind_event_cmd_args_cb) +cmdArgEntry.grid(column=1, row=2, sticky=N+W, pady=0, padx=5) + + +# ############################################################################################### # +# PARAMETERS FRAME +# ############################################################################################### # +# conversion_tab_left_part_row_num += 1 + +column_num_arg_name = '-col' # -col +resolution_multiplier_arg_name = '-dr' # -dr +crop_margin_arg_name = '-cbox' # -cbox[|u|-] +dpi_arg_name = '-dpi' # -dpi +page_num_arg_name = '-p' # -p + +isColumnNum = BooleanVar() +isResolutionMultipler = BooleanVar() +isCropMargin = BooleanVar() +isDPI = BooleanVar() + +strvarColumnNum = StringVar() +strvarResolutionMultiplier = StringVar() +strvarCropPageRange = StringVar() +strvarLeftMargin = StringVar() +strvarRightMargin = StringVar() +strvarTopMargin = StringVar() +strvarBottomMargin = StringVar() +strvarDPI = StringVar() +strvarPageNums = StringVar() + +parameters_frame = Labelframe(conversionTab, text='Parameters') +parameters_frame.grid( + column=conversion_tab_left_part_column_num, + row=conversion_tab_left_part_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num = 0 + + +def on_command_column_num_cb(): + if isColumnNum.get(): + arg = ( + column_num_arg_name + ' ' + + strvarColumnNum.get().strip() + ) + add_or_update_one_cmd_arg(column_num_arg_name, arg) + else: + remove_one_cmd_arg(column_num_arg_name) + +colNumCheckButton = Checkbutton( + parameters_frame, + text='Max Columns', + variable=isColumnNum, + command=on_command_column_num_cb, +) +colNumCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +colNumSpinBox = Spinbox( + parameters_frame, + from_=1, + to=10, + increment=1, + state='readonly', + textvariable=strvarColumnNum, + command=on_command_column_num_cb, +) +colNumSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + + +def on_command_resolution_multipler_cb(): + if isResolutionMultipler.get(): + arg = ( + resolution_multiplier_arg_name + ' ' + + strvarResolutionMultiplier.get().strip() + ) + add_or_update_one_cmd_arg(resolution_multiplier_arg_name, arg) + else: + remove_one_cmd_arg(resolution_multiplier_arg_name) + +resolutionCheckButton = Checkbutton( + parameters_frame, + text='Document Resolution Factor', + variable=isResolutionMultipler, + command=on_command_resolution_multipler_cb, +) +resolutionCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +resolutionSpinBox = Spinbox( + parameters_frame, + from_=0.1, + to=10.0, + increment=0.1, + state='readonly', + textvariable=strvarResolutionMultiplier, + command=on_command_resolution_multipler_cb, +) +resolutionSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + + +def on_command_and_validate_crop_margin_cb(): + if (len(strvarCropPageRange.get().strip()) > 0 and + not check_page_nums(strvarCropPageRange.get().strip())): + remove_one_cmd_arg(crop_margin_arg_name) + strvarCropPageRange.set('') + + tkinter.messagebox.showerror( + message='Invalide Crop Page Range! Should be like 2-5e,3-7o,9-', + ) + + return FALSE + + if isCropMargin.get(): + page_range_arg = strvarCropPageRange.get().strip() + margin_args = [ + strvarLeftMargin.get(), + strvarTopMargin.get(), + strvarRightMargin.get(), + strvarBottomMargin.get(), + ] + arg = ( + # no space between -cbox and page range + crop_margin_arg_name + page_range_arg + ' ' + + 'in,'.join(map(str.strip, margin_args)) + 'in' + ) + add_or_update_one_cmd_arg(crop_margin_arg_name, arg) + else: + remove_one_cmd_arg(crop_margin_arg_name) + +marginCheckButton = Checkbutton( + parameters_frame, + text='Crop Margins (in)', + variable=isCropMargin, + command=on_command_and_validate_crop_margin_cb, +) +marginCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +cropPageRangeTextLabel = Label(parameters_frame, text=' Page Range') +cropPageRangeTextLabel.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +cropPageRangeEntry = Entry( + parameters_frame, + textvariable=strvarCropPageRange, + validate='focusout', + validatecommand=on_command_and_validate_crop_margin_cb, +) +cropPageRangeEntry.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +leftMarginTextLabel = Label(parameters_frame, text=' Left Margin') +leftMarginTextLabel.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +leftMarginSpinBox = Spinbox( + parameters_frame, + from_=0, + to=100, + increment=0.01, + # state='readonly', + textvariable=strvarLeftMargin, + command=on_command_and_validate_crop_margin_cb, +) +leftMarginSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +topMarginTextLabel = Label(parameters_frame, text=' Top Margin') +topMarginTextLabel.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +topMarginSpinBox = Spinbox( + parameters_frame, + from_=0, + to=100, + increment=0.01, + # state='readonly', + textvariable=strvarTopMargin, + command=on_command_and_validate_crop_margin_cb, +) +topMarginSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +rightMarginTextLabel = Label(parameters_frame, text=' Width') +rightMarginTextLabel.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +rightMarginSpinBox = Spinbox( + parameters_frame, + from_=0, + to=100, + increment=0.01, + # state='readonly', + textvariable=strvarRightMargin, + command=on_command_and_validate_crop_margin_cb, +) +rightMarginSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + +bottomMarginTextLabel = Label(parameters_frame, text=' Height') +bottomMarginTextLabel.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +bottomMarginSpinBox = Spinbox( + parameters_frame, + from_=0, + to=100, + increment=0.01, + # state='readonly', + textvariable=strvarBottomMargin, + command=on_command_and_validate_crop_margin_cb, +) +bottomMarginSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + + +def on_command_dpi_cb(): + if isDPI.get(): + arg = dpi_arg_name + ' ' + strvarDPI.get().strip() + add_or_update_one_cmd_arg(dpi_arg_name, arg) + else: + remove_one_cmd_arg(dpi_arg_name) + +dpiCheckButton = Checkbutton( + parameters_frame, + text='DPI', + variable=isDPI, + command=on_command_dpi_cb, +) +dpiCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +dpiSpinBox = Spinbox( + parameters_frame, + from_=0, + to=1000, + increment=1, + # state='readonly', + textvariable=strvarDPI, + command=on_command_dpi_cb, +) +dpiSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + + +def validate_and_update_page_nums(): + if (len(strvarPageNums.get().strip()) > 0 and + not check_page_nums(strvarPageNums.get().strip())): + remove_one_cmd_arg(page_num_arg_name) + strvarPageNums.set('') + + tkinter.messagebox.showerror( + message='Invalide Page Argument! Should be like 2-5e,3-7o,9-', + ) + + return FALSE + + if len(strvarPageNums.get().strip()) > 0: + arg = page_num_arg_name + ' ' + strvarPageNums.get().strip() + add_or_update_one_cmd_arg(page_num_arg_name, arg) + else: + remove_one_cmd_arg(page_num_arg_name) + + return TRUE + +pageNumTextLabel = Label( + parameters_frame, + text='Pages to Convert', +) +pageNumTextLabel.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + + +def on_validate_page_nums_cb(): + validate_and_update_page_nums() + +pageNumEntry = Entry( + parameters_frame, + textvariable=strvarPageNums, + validate='focusout', + validatecommand=on_validate_page_nums_cb, +) +pageNumEntry.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +fixed_font_size_arg_name = '-fs' # -fs 0/-fs [+] +ocr_arg_name = '-ocr' # -ocr-/-ocr t +ocr_cpu_arg_name = '-nt' # -nt -50/-nt +landscape_arg_name = '-ls' # -ls[-][pagelist] +linebreak_arg_name = '-ws' # -ws + +isFixedFontSize = BooleanVar() +isOCR = BooleanVar() +isLandscape = BooleanVar() +isLinebreak = BooleanVar() # -ws 0.01~10 + +strvarFixedFontSize = StringVar() +strvarOcrCpuPercentage = StringVar() +strvarLandscapePages = StringVar() # 1,3,5-10 +strvarLinebreakSpace = StringVar() + +# checkbox with value options +para_frame_row_num += 1 + + +def on_command_fixed_font_size_cb(): + if isFixedFontSize.get(): + arg = ( + fixed_font_size_arg_name + ' ' + + strvarFixedFontSize.get().strip() + ) + add_or_update_one_cmd_arg( + fixed_font_size_arg_name, + arg, + ) + else: + remove_one_cmd_arg(fixed_font_size_arg_name) + +fontSizeCheckButton = Checkbutton( + parameters_frame, + text='Fixed Output Font Size', + variable=isFixedFontSize, + command=on_command_fixed_font_size_cb, +) +fontSizeCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +fontSizeSpinBox = Spinbox( + parameters_frame, + from_=0, + to=100, + increment=1, + state='readonly', + textvariable=strvarFixedFontSize, + command=on_command_fixed_font_size_cb, +) +fontSizeSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + + +def on_command_ocr_and_cpu_cb(): + if isOCR.get(): + # ocr conflicts with native pdf + isNativePdf.set(FALSE) + remove_one_cmd_arg(native_pdf_arg_name) + + ocr_arg = ocr_arg_name + add_or_update_one_cmd_arg(ocr_arg_name, ocr_arg) + + # negtive integer means percentage + ocr_cpu_arg = ( + ocr_cpu_arg_name + '-' + + strvarOcrCpuPercentage.get().strip() + ) + add_or_update_one_cmd_arg( + ocr_cpu_arg_name, + ocr_cpu_arg, + ) + else: + remove_one_cmd_arg(ocr_arg_name) + remove_one_cmd_arg(ocr_cpu_arg_name) + +ocrCheckButton = Checkbutton( + parameters_frame, + text='OCR (Tesseract) and CPU %', + variable=isOCR, + command=on_command_ocr_and_cpu_cb, +) +ocrCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +ocrCpuSpinBox = Spinbox( + parameters_frame, + from_=0, + to=100, + increment=1, + state='readonly', + textvariable=strvarOcrCpuPercentage, + command=on_command_ocr_and_cpu_cb, +) +ocrCpuSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + + +def on_command_and_validate_landscape_cb(): + if (len(strvarLandscapePages.get().strip()) > 0 and + not check_page_nums(strvarLandscapePages.get().strip())): + remove_one_cmd_arg(landscape_arg_name) + strvarLandscapePages.set('') + + tkinter.messagebox.showerror( + message='Invalide Landscape Page Argument!', + ) + + return FALSE + + if isLandscape.get(): + arg = '-ls' + if len(strvarLandscapePages.get().strip()) > 0: + # no space between -ls and page numbers + arg += strvarLandscapePages.get() + + add_or_update_one_cmd_arg(landscape_arg_name, arg.strip()) + else: + remove_one_cmd_arg(landscape_arg_name) + + return TRUE + +landscapeCheckButton = Checkbutton( + parameters_frame, + text='Output in Landscape', + variable=isLandscape, + command=on_command_and_validate_landscape_cb, +) +landscapeCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +landscapePageNumEntry = Entry( + parameters_frame, + textvariable=strvarLandscapePages, + validate='focusout', + validatecommand=on_command_and_validate_landscape_cb, +) +landscapePageNumEntry.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +para_frame_row_num += 1 + + +def on_command_line_break_cb(): + if isLinebreak.get(): + arg = ( + linebreak_arg_name + ' ' + + strvarLinebreakSpace.get().strip() + ) + add_or_update_one_cmd_arg(linebreak_arg_name, arg) + else: + remove_one_cmd_arg(linebreak_arg_name) + +lineBreakCheckButton = Checkbutton( + parameters_frame, + text='Smart Line Breaks', + variable=isLinebreak, + command=on_command_line_break_cb, +) +lineBreakCheckButton.grid( + column=0, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +lineBreakSpinBox = Spinbox( + parameters_frame, + from_=0.01, + to=2.00, + increment=0.01, + state='readonly', + textvariable=strvarLinebreakSpace, + command=on_command_line_break_cb, +) +lineBreakSpinBox.grid( + column=1, + row=para_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +# right side of conversion tab +conversion_tab_right_part_column_num = 1 +conversion_tab_right_part_row_num = -1 + + +# ############################################################################################### # +# OPTIONS FRAME +# ############################################################################################### # +conversion_tab_right_part_row_num += 1 + +auto_straignten_arg_name = '-as' # -as-/-as +break_page_avoid_overlap_arg_name = '-bp' # -bp-/-bp +color_output_arg_name = '-c' # -c-/-c +native_pdf_arg_name = '-n' # -n-/-n +right_to_left_arg_name = '-r' # -r-/-r +post_gs_arg_name = '-ppgs' # -ppgs-/-ppgs +marked_source_arg_name = '-sm' # -sm-/-sm +reflow_text_arg_name = '-wrap' # -wrap+/-wrap- +erase_vertical_line_arg_name = '-evl' # -evl 0/-evl 1 +erase_horizontal_line_arg_name = '-ehl' # -ehl 0/-ehl 1 +fast_preview_arg_name = '-rt' # -rt /-rt 0 +ign_small_defects_arg_name = '-de' # -de 1.0/-de 1.5 +auto_crop_arg_name = '-ac' # -ac-/-ac + +isAutoStraighten = BooleanVar() +isBreakPage = BooleanVar() +isColorOutput = BooleanVar() +isNativePdf = BooleanVar() +isRight2Left = BooleanVar() +isPostGs = BooleanVar() +isMarkedSrc = BooleanVar() +isReflowText = BooleanVar() +isEraseVerticalLine = BooleanVar() +isEraseHorizontalLine = BooleanVar() +isFastPreview = BooleanVar() +isAvoidOverlap = BooleanVar() +isIgnSmallDefects = BooleanVar() +isAutoCrop = BooleanVar() + +options_frame = Labelframe( + conversionTab, + text='Options', +) +options_frame.grid( + column=conversion_tab_right_part_column_num, + row=conversion_tab_right_part_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +option_frame_left_part_col_num = 0 +option_frame_row_num = 0 + + +def on_command_auto_straighten_cb(): + if isAutoStraighten.get(): + arg = auto_straignten_arg_name + add_or_update_one_cmd_arg(auto_straignten_arg_name, arg) + else: + remove_one_cmd_arg(auto_straignten_arg_name) + + +opt1 = Checkbutton( + options_frame, + text='Autostraighten', + variable=isAutoStraighten, + command=on_command_auto_straighten_cb, +) +opt1.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + + +def on_command_break_page_cb(): + if isBreakPage.get(): + # break page conflicts with avoid overlap since they are both -bp flag + isAvoidOverlap.set(FALSE) + remove_one_cmd_arg(break_page_avoid_overlap_arg_name) + + arg = break_page_avoid_overlap_arg_name + add_or_update_one_cmd_arg( + break_page_avoid_overlap_arg_name, + arg, + ) + else: + remove_one_cmd_arg(break_page_avoid_overlap_arg_name) + +opt2 = Checkbutton( + options_frame, + text='Break After Each Source Page', + variable=isBreakPage, + command=on_command_break_page_cb, +) +opt2.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + + +def on_command_color_output_cb(): + if isColorOutput.get(): + arg = color_output_arg_name + add_or_update_one_cmd_arg(color_output_arg_name, arg) + else: + remove_one_cmd_arg(color_output_arg_name) + +opt3 = Checkbutton( + options_frame, + text='Color Output', + variable=isColorOutput, + command=on_command_color_output_cb, +) +opt3.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + + +def on_command_native_pdf_cb(): + if isNativePdf.get(): + # native pdf conflicts with ocr and reflow text + isOCR.set(FALSE) + remove_one_cmd_arg(ocr_arg_name) + remove_one_cmd_arg(ocr_cpu_arg_name) + + isReflowText.set(FALSE) + remove_one_cmd_arg(reflow_text_arg_name) + + arg = native_pdf_arg_name + add_or_update_one_cmd_arg(native_pdf_arg_name, arg) + else: + remove_one_cmd_arg(native_pdf_arg_name) + +opt4 = Checkbutton( + options_frame, + text='Native PDF Output', + variable=isNativePdf, + command=on_command_native_pdf_cb, +) +opt4.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + + +def on_command_right_to_left_cb(): + if isRight2Left.get(): + arg = right_to_left_arg_name + add_or_update_one_cmd_arg(right_to_left_arg_name, arg) + else: + remove_one_cmd_arg(right_to_left_arg_name) + +opt5 = Checkbutton( + options_frame, + text='Right-to-Left Text', + variable=isRight2Left, + command=on_command_right_to_left_cb, +) +opt5.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + + +def on_command_post_gs_cb(): + if isPostGs.get(): + arg = post_gs_arg_name + add_or_update_one_cmd_arg(post_gs_arg_name, arg) + else: + remove_one_cmd_arg(post_gs_arg_name) + +opt6 = Checkbutton( + options_frame, + text='Post Process w/GhostScript', + variable=isPostGs, + command=on_command_post_gs_cb, +) +opt6.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + + +def on_command_marked_src_cb(): + if isMarkedSrc.get(): + arg = marked_source_arg_name + add_or_update_one_cmd_arg( + marked_source_arg_name, + arg, + ) + else: + remove_one_cmd_arg(marked_source_arg_name) + +opt7 = Checkbutton( + options_frame, + text='Generate Marked-up Source', + variable=isMarkedSrc, + command=on_command_marked_src_cb, +) +opt7.grid( + column=option_frame_left_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +option_frace_right_part_col_num = 1 +option_frame_row_num = 0 + + +def on_command_reflow_text_cb(): + if isReflowText.get(): + # reflow text conflicts with native pdf + isNativePdf.set(FALSE) + remove_one_cmd_arg(native_pdf_arg_name) + + arg = reflow_text_arg_name + '+' + add_or_update_one_cmd_arg( + reflow_text_arg_name, + arg, + ) + else: + remove_one_cmd_arg(reflow_text_arg_name) + +opt8 = Checkbutton( + options_frame, + text='Re-flow Text', + variable=isReflowText, + command=on_command_reflow_text_cb, +) +opt8.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + + +def on_command_erase_vertical_line_cb(): + if isEraseVerticalLine.get(): + arg = erase_vertical_line_arg_name + ' 1' + add_or_update_one_cmd_arg( + erase_vertical_line_arg_name, + arg, + ) + else: + remove_one_cmd_arg(erase_vertical_line_arg_name) + +opt9 = Checkbutton( + options_frame, + text='Erase Vertical Lines', + variable=isEraseVerticalLine, + command=on_command_erase_vertical_line_cb, +) +opt9.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + + +def on_command_erase_horizontal_line_cb(): + if isEraseHorizontalLine.get(): + arg = erase_horizontal_line_arg_name + ' 1' + add_or_update_one_cmd_arg( + erase_horizontal_line_arg_name, + arg, + ) + else: + remove_one_cmd_arg(erase_horizontal_line_arg_name) + +opt14 = Checkbutton( + options_frame, + text='Erase Horizontal Lines', + variable=isEraseHorizontalLine, + command=on_command_erase_horizontal_line_cb, +) +opt14.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + + +def on_command_fast_preview_cb(): + if isFastPreview.get(): + arg = fast_preview_arg_name + ' 0' + add_or_update_one_cmd_arg(fast_preview_arg_name, arg) + else: + remove_one_cmd_arg(fast_preview_arg_name) + +opt10 = Checkbutton( + options_frame, + text='Fast Preview', + variable=isFastPreview, + command=on_command_fast_preview_cb, +) +opt10.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + + +def on_command_avoid_text_selection_overlap_cb(): + if isAvoidOverlap.get(): + # avoid overlap conflicts with break page since they are both -bp flag + isBreakPage.set(FALSE) + remove_one_cmd_arg(break_page_avoid_overlap_arg_name) + + arg = break_page_avoid_overlap_arg_name + ' m' + add_or_update_one_cmd_arg( + break_page_avoid_overlap_arg_name, + arg, + ) + else: + remove_one_cmd_arg(break_page_avoid_overlap_arg_name) + +opt11 = Checkbutton( + options_frame, + text='Avoid Text Selection Overlap', + variable=isAvoidOverlap, + command=on_command_avoid_text_selection_overlap_cb, +) +opt11.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + + +def on_command_ign_small_defect_cb(): + if isIgnSmallDefects.get(): + arg = ( + ign_small_defects_arg_name + ' 1.5' + ) + add_or_update_one_cmd_arg( + ign_small_defects_arg_name, + arg, + ) + else: + remove_one_cmd_arg(ign_small_defects_arg_name) + +opt12 = Checkbutton( + options_frame, + text='Ignore Small Defects', + variable=isIgnSmallDefects, + command=on_command_ign_small_defect_cb, +) +opt12.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + + +def on_command_auto_crop_cb(): + if isAutoCrop.get(): + arg = auto_crop_arg_name + add_or_update_one_cmd_arg(auto_crop_arg_name, arg) + else: + remove_one_cmd_arg(auto_crop_arg_name) + +opt13 = Checkbutton( + options_frame, + text='Auto-Crop', + variable=isAutoCrop, + command=on_command_auto_crop_cb, +) +opt13.grid( + column=option_frace_right_part_col_num, + row=option_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +option_frame_row_num += 1 + + +# ############################################################################################### # +# PREVIEW AND CONVERT FRAME +# ############################################################################################### # +conversion_tab_right_part_row_num += 1 + +preview_output_arg_name = '-bmp' +preview_image_path = './k2pdfopt_out.png' +current_preview_page_index = 1 + +# global variable to hold opened preview image to prevent gc collecting it +preview_img = None +canvas_image_tag = None + +strvarCurrentPreviewPageNum = StringVar() + +def remove_preview_img_and_clear_canvas(): + global canvas_image_tag + global strvarCurrentPreviewPageNum + + if os.path.exists(preview_image_path): + os.remove(preview_image_path) + + previewImageCanvas.delete(ALL) + canvas_image_tag = None + + +def load_image_to_canvas(photo_img, canvas): + load_image_to_canvas(preview_img, previewImageCanvas) + + +def load_preview_image(img_path, preview_page_index): + # PhotoImage must be global var to prevent gc collect it + global preview_img + global previewImageCanvas + + if os.path.exists(img_path): + preview_img = PhotoImage(file=img_path) + + canvas_image_tag = previewImageCanvas.create_image( + (0, 0), + anchor=NW, + image=preview_img, + tags='preview', + ) + + (left_pos, top_pos, right_pos, bottom_pos) = ( + 0, + 0, + preview_img.width(), + preview_img.height(), + ) + previewImageCanvas.config( + scrollregion=(left_pos, top_pos, right_pos, bottom_pos), + ) + # canvas.scale('preview', 0, 0, 0.1, 0.1) + strvarCurrentPreviewPageNum.set('Page: ' + str(preview_page_index)) + else: + strvarCurrentPreviewPageNum.set('No Page ' + str(preview_page_index)) + + +def generate_one_preview_image(preview_page_index): + global background_future + + if not check_pdf_conversion_done(): + return + + if not os.path.exists(strvarFilePath.get().strip()): + tkinter.messagebox.showerror( + message=( + "Failed to Find Input PDF File to convert for Preview: %s" + % + strvarFilePath.get().strip() + ), + ) + return + + remove_preview_img_and_clear_canvas() + + (base_path, file_ext) = os.path.splitext(strvarFilePath.get().strip()) + output_arg = ' '.join([preview_output_arg_name, str(preview_page_index)]) + background_future = convert_pdf_file(output_arg) + strvarCurrentPreviewPageNum.set('Preview Generating...') + + def preview_image_future_cb(bgf): + load_preview_image(preview_image_path, preview_page_index) + log_string( + "Preview generation for page %d finished" % + preview_page_index + ) + + background_future.add_done_callback(preview_image_future_cb) + +preview_frame = Labelframe(conversionTab, text='Preview & Convert') +preview_frame.grid( + column=conversion_tab_right_part_column_num, + row=conversion_tab_right_part_row_num, + rowspan=3, + sticky=N+S+E+W, + pady=0, + padx=5, +) + +preview_frame_row_num = 0 + + +def on_command_restore_default_cb(): + restore_default_values() + +reset_button = Button( + preview_frame, + text='Reset default', + command=on_command_restore_default_cb, +) +reset_button.grid( + column=0, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + + +def on_command_abort_conversion_cb(): + global background_future + global background_process + + if background_future is not None: + background_future.cancel() + + if (background_process is not None and + background_process.returncode is None): + background_process.terminate() + +cancel_button = Button( + preview_frame, + text='Abort', + command=on_command_abort_conversion_cb, +) +cancel_button.grid( + column=1, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + + +def on_command_convert_pdf_cb(): + if not check_pdf_conversion_done(): + return + + global background_future + + pdf_output_arg = output_path_arg_name + ' %s' + output_pdf_suffix + background_future = convert_pdf_file(pdf_output_arg) + +convert_button = Button( + preview_frame, + text='Convert', + command=on_command_convert_pdf_cb, +) +convert_button.grid( + column=2, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +preview_frame_row_num += 1 + +current_preview_page_num_entry = Entry( + preview_frame, + state='readonly', + textvariable=strvarCurrentPreviewPageNum) +current_preview_page_num_entry.grid( + column=0, + row=preview_frame_row_num, + columnspan=2, + sticky=N+W, + pady=0, + padx=5, +) + + +def on_command_ten_page_up_cb(): + global current_preview_page_index + current_preview_page_index -= 10 + if current_preview_page_index < 1: + current_preview_page_index = 1 + generate_one_preview_image(current_preview_page_index) + +preview_button = Button( + preview_frame, + text='Preview', + command=on_command_ten_page_up_cb, +) +preview_button.grid( + column=2, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +preview_frame_column_num = 0 +preview_frame_row_num += 1 + +first_button = Button( + preview_frame, + text='<<', + command=on_command_ten_page_up_cb, +) +first_button.grid( + column=preview_frame_column_num, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +preview_frame_column_num += 1 + + +def on_command_page_up_cb(): + global current_preview_page_index + if current_preview_page_index > 1: + current_preview_page_index -= 1 + generate_one_preview_image(current_preview_page_index) + +previous_button = Button( + preview_frame, + text='<', + command=on_command_page_up_cb, +) +previous_button.grid( + column=preview_frame_column_num, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +preview_frame_column_num += 1 + + +def on_command_page_down_cb(): + global current_preview_page_index + current_preview_page_index += 1 + generate_one_preview_image(current_preview_page_index) + +next_button = Button( + preview_frame, + text='>', + command=on_command_page_down_cb, +) +next_button.grid( + column=preview_frame_column_num, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) +preview_frame_column_num += 1 + + +def on_command_ten_page_down_cb(): + global current_preview_page_index + current_preview_page_index += 10 + generate_one_preview_image(current_preview_page_index) + +last_button = Button( + preview_frame, + text='>>', + command=on_command_ten_page_down_cb, +) +last_button.grid( + column=preview_frame_column_num, + row=preview_frame_row_num, + sticky=N+W, + pady=0, + padx=5, +) + +preview_frame_column_num += 1 +preview_frame_row_num += 1 + +xScrollBar = Scrollbar(preview_frame, orient=HORIZONTAL) +xScrollBar.grid( + column=0, + row=preview_frame_row_num+1, + columnspan=preview_frame_column_num, + sticky=E+W, +) + +yScrollBar = Scrollbar(preview_frame) +yScrollBar.grid( + column=preview_frame_column_num, + row=preview_frame_row_num, + sticky=N+S, +) + +previewImageCanvas = Canvas( + preview_frame, + bd=0, + xscrollcommand=xScrollBar.set, + yscrollcommand=yScrollBar.set, +) +previewImageCanvas.grid( + column=0, + row=preview_frame_row_num, + columnspan=preview_frame_column_num, + sticky=N+S+E+W, +) + +xScrollBar.config(command=previewImageCanvas.xview) +yScrollBar.config(command=previewImageCanvas.yview) + +conversionTab.columnconfigure( + conversion_tab_right_part_column_num, + weight=1, +) +conversionTab.rowconfigure( + conversion_tab_right_part_row_num, + weight=1, +) +preview_frame.columnconfigure(0, weight=1) +preview_frame.rowconfigure(preview_frame_row_num, weight=1) + + +def yscroll_canvas(event): + previewImageCanvas.yview_scroll(-1 * event.delta, 'units') + + +def xscroll_canvas(event): + previewImageCanvas.xview_scroll(-1 * event.delta, 'units') + +previewImageCanvas.bind('', yscroll_canvas) +previewImageCanvas.bind("", xscroll_canvas) + +preview_frame_row_num += 1 + +# collect all vars + +bool_var_list = [ + isColumnNum, + isResolutionMultipler, + isCropMargin, + isDPI, + isFixedFontSize, + isOCR, + isLandscape, + isLinebreak, + + isAutoStraighten, + isBreakPage, + isColorOutput, + isNativePdf, + isRight2Left, + isPostGs, + isMarkedSrc, + isReflowText, + isEraseVerticalLine, + isEraseHorizontalLine, + isFastPreview, + isAvoidOverlap, + isIgnSmallDefects, + isAutoCrop, +] + +string_var_list = [ + strvarFilePath, + strvarDevice, + strvarConversionMode, + strvarScreenUnit, + strvarScreenWidth, + strvarScreenHeight, + + strvarOutputFilePath, + strvarCmdArgs, + + strvarColumnNum, + strvarResolutionMultiplier, + strvarCropPageRange, + strvarLeftMargin, + strvarRightMargin, + strvarTopMargin, + strvarBottomMargin, + strvarDPI, + strvarPageNums, + + strvarFixedFontSize, + strvarOcrCpuPercentage, + strvarLandscapePages, + strvarLinebreakSpace, + + strvarCurrentPreviewPageNum, +] + +combo_box_list = [ + deviceComboBox, + modeComboBox, + unitComboBox, +] + +entry_list = [ + inputPathEntry, + outputPathEntry, + cmdArgEntry, + pageNumEntry, + landscapePageNumEntry, + current_preview_page_num_entry, +] + +default_var_map = { + device_arg_name: ['Kindle Paperwhite 3'], + screen_unit_prefix: ['Pixels'], + width_arg_name: ['1024'], + height_arg_name: ['768'], + conversion_mode_arg_name: ['Default'], + output_path_arg_name: [''], + + column_num_arg_name: [FALSE, '2'], + resolution_multiplier_arg_name: [FALSE, '1.0'], + crop_margin_arg_name: [ + FALSE, + '', + '0.00', + '0.00', + '0.00', + '0.00', + ], + dpi_arg_name: [TRUE, '300'], + page_num_arg_name: [''], + fixed_font_size_arg_name: [FALSE, '12'], + ocr_arg_name: [FALSE, '50'], + ocr_cpu_arg_name: [FALSE, '50'], + landscape_arg_name: [FALSE, ''], + linebreak_arg_name: [TRUE, '0.200'], + + auto_straignten_arg_name: [FALSE], + break_page_avoid_overlap_arg_name: [FALSE, FALSE], + color_output_arg_name: [FALSE], + native_pdf_arg_name: [FALSE], + right_to_left_arg_name: [FALSE], + post_gs_arg_name: [FALSE], + marked_source_arg_name: [TRUE], + reflow_text_arg_name: [TRUE], + erase_vertical_line_arg_name: [TRUE], + erase_horizontal_line_arg_name: [TRUE], + fast_preview_arg_name: [TRUE], + ign_small_defects_arg_name: [FALSE], + auto_crop_arg_name: [FALSE], + + preview_output_arg_name: [] +} + +arg_var_map = { + device_arg_name: [strvarDevice], + screen_unit_prefix: [strvarScreenUnit], + width_arg_name: [strvarScreenWidth], + height_arg_name: [strvarScreenHeight], + conversion_mode_arg_name: [strvarConversionMode], + output_path_arg_name: [strvarOutputFilePath], + + column_num_arg_name: [ + isColumnNum, + strvarColumnNum, + ], + resolution_multiplier_arg_name: [ + isResolutionMultipler, + strvarResolutionMultiplier, + ], + crop_margin_arg_name: [ + isCropMargin, + strvarCropPageRange, + strvarLeftMargin, + strvarTopMargin, + strvarRightMargin, + strvarBottomMargin, + ], + dpi_arg_name: [ + isDPI, + strvarDPI, + ], + page_num_arg_name: [ + strvarPageNums, + ], + + fixed_font_size_arg_name: [ + isFixedFontSize, + strvarFixedFontSize, + ], + ocr_arg_name: [ + isOCR, + strvarOcrCpuPercentage, + ], + ocr_cpu_arg_name: [ + isOCR, + strvarOcrCpuPercentage, + ], + landscape_arg_name: [ + isLandscape, + strvarLandscapePages, + ], + linebreak_arg_name: [ + isLinebreak, + strvarLinebreakSpace, + ], + + auto_straignten_arg_name: [isAutoStraighten], + break_page_avoid_overlap_arg_name: [isBreakPage, isAvoidOverlap], + color_output_arg_name: [isColorOutput], + native_pdf_arg_name: [isNativePdf], + right_to_left_arg_name: [isRight2Left], + post_gs_arg_name: [isPostGs], + marked_source_arg_name: [isMarkedSrc], + reflow_text_arg_name: [isReflowText], + erase_vertical_line_arg_name: [isEraseVerticalLine], + erase_horizontal_line_arg_name: [isEraseHorizontalLine], + fast_preview_arg_name: [isFastPreview], + # break_page_avoid_overlap_arg_name: [] + ign_small_defects_arg_name: [isIgnSmallDefects], + auto_crop_arg_name: [isAutoCrop], + preview_output_arg_name: [] +} + +arg_cb_map = { + device_arg_name: on_bind_event_device_unit_cb, + width_arg_name: on_command_width_height_cb, + height_arg_name: on_command_width_height_cb, + conversion_mode_arg_name: on_bind_event_mode_cb, + output_path_arg_name: None, + + column_num_arg_name: on_command_column_num_cb, + resolution_multiplier_arg_name: on_command_resolution_multipler_cb, + crop_margin_arg_name: on_command_and_validate_crop_margin_cb, + dpi_arg_name: on_command_dpi_cb, + page_num_arg_name: on_validate_page_nums_cb, + + fixed_font_size_arg_name: on_command_fixed_font_size_cb, + ocr_arg_name: on_command_ocr_and_cpu_cb, + ocr_cpu_arg_name: on_command_ocr_and_cpu_cb, + landscape_arg_name: on_command_and_validate_landscape_cb, + linebreak_arg_name: on_command_line_break_cb, + + auto_straignten_arg_name: on_command_auto_straighten_cb, + break_page_avoid_overlap_arg_name: on_command_break_page_cb, + color_output_arg_name: on_command_color_output_cb, + native_pdf_arg_name: on_command_native_pdf_cb, + right_to_left_arg_name: on_command_right_to_left_cb, + post_gs_arg_name: on_command_post_gs_cb, + marked_source_arg_name: on_command_marked_src_cb, + reflow_text_arg_name: on_command_reflow_text_cb, + erase_vertical_line_arg_name: on_command_erase_vertical_line_cb, + erase_horizontal_line_arg_name: on_command_erase_horizontal_line_cb, + fast_preview_arg_name: on_command_fast_preview_cb, + ign_small_defects_arg_name: on_command_ign_small_defect_cb, + auto_crop_arg_name: on_command_auto_crop_cb, + + preview_output_arg_name: None +} + +# k2pdfopt stdout + +output_frame = Labelframe(logTab, text='k2pdfopt STDOUT') +output_frame.pack(expand=1, fill='both') + +def on_command_clear_log_cb(): + clear_logs() + +clearButton = Button( + output_frame, + text='Clear', + command=on_command_clear_log_cb) +clearButton.grid( + column=0, + row=0, + sticky=N+W, + pady=0, + padx=5, +) + +stdoutText = scrltxt.ScrolledText( + output_frame, + state=DISABLED, + wrap='word', +) +stdoutText.grid(column=0, row=1, sticky=N+S+E+W) +output_frame.columnconfigure(0, weight=1) +output_frame.rowconfigure(1, weight=1) +# stdoutText.pack(expand=1, fill='both') + +# initialization + +def initialize(): + check_k2pdfopt_path_exists() + + if not load_custom_preset(): + restore_default_values() + + pwd = os.getcwd() + log_string('Current directory: ' + pwd) + +initialize() + +# start TclTk loop + +root.mainloop()