From 3b16235821afafc4bb29a94059037fe138ed0907 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 24 Apr 2024 22:38:32 -0400 Subject: [PATCH] move print PDF back to web browser (#2849) * move print PDF back to web browser * fix line wrapping for macOS --------- Co-authored-by: Kaalleen --- lib/extensions/print_pdf.py | 159 ++++++++++++++++++++++++++++++++--- print/resources/inkstitch.js | 40 +++++++-- print/resources/style.css | 2 +- print/templates/ui.html | 1 - 4 files changed, 179 insertions(+), 23 deletions(-) diff --git a/lib/extensions/print_pdf.py b/lib/extensions/print_pdf.py index c3c14e489..6ee051a1d 100644 --- a/lib/extensions/print_pdf.py +++ b/lib/extensions/print_pdf.py @@ -9,25 +9,27 @@ import os import socket import sys import time +import webbrowser +from contextlib import closing from copy import deepcopy from datetime import date from threading import Thread -from contextlib import closing import appdirs +import wx from flask import Flask, Response, jsonify, request, send_from_directory from jinja2 import Environment, FileSystemLoader, select_autoescape from lxml import etree from werkzeug.serving import make_server -from ..gui import open_url -from ..i18n import get_languages +from .base import InkstitchExtension +from ..debug import debug +from ..i18n import _, get_languages from ..i18n import translation as inkstitch_translation from ..stitch_plan import stitch_groups_to_stitch_plan from ..svg import render_stitch_plan from ..svg.tags import INKSCAPE_GROUPMODE from ..threads import ThreadCatalog -from .base import InkstitchExtension def datetimeformat(value, format='%Y/%m/%d'): @@ -57,6 +59,42 @@ def save_defaults(defaults): json.dump(defaults, defaults_file) +def open_url(url): + # Avoid spurious output from xdg-open. Any output on stdout will crash + # inkscape. + null = open(os.devnull, 'w') + old_stdout = os.dup(sys.stdout.fileno()) + os.dup2(null.fileno(), sys.stdout.fileno()) + + if getattr(sys, 'frozen', False): + + # PyInstaller sets LD_LIBRARY_PATH. We need to temporarily clear it + # to avoid confusing xdg-open, which webbrowser will run. + + # The following code is adapted from PyInstaller's documentation + # http://pyinstaller.readthedocs.io/en/stable/runtime-information.html + + old_environ = dict(os.environ) # make a copy of the environment + lp_key = 'LD_LIBRARY_PATH' # for Linux and *BSD. + lp_orig = os.environ.get(lp_key + '_ORIG') # pyinstaller >= 20160820 has this + if lp_orig is not None: + os.environ[lp_key] = lp_orig # restore the original, unmodified value + else: + os.environ.pop(lp_key, None) # last resort: remove the env var + + webbrowser.open(url) + + # restore the old environ + os.environ.clear() + os.environ.update(old_environ) + else: + webbrowser.open(url) + + # restore file descriptors + os.dup2(old_stdout, sys.stdout.fileno()) + os.close(old_stdout) + + class PrintPreviewServer(Thread): def __init__(self, *args, **kwargs): self.html = kwargs.pop('html') @@ -66,8 +104,11 @@ class PrintPreviewServer(Thread): self.realistic_color_block_svgs = kwargs.pop('realistic_color_block_svgs') Thread.__init__(self, *args, **kwargs) self.daemon = True + self.last_request_time = None + self.shutting_down = False self.flask_server = None self.server_thread = None + self.started = False self.__setup_app() @@ -89,14 +130,33 @@ class PrintPreviewServer(Thread): self.app = Flask(__name__) + self.watcher_thread = Thread(target=self.watch) + self.watcher_thread.daemon = True + self.watcher_thread.start() + + @self.app.before_request + def request_started(): + self.last_request_time = time.time() + @self.app.route('/') def index(): return self.html + @self.app.route('/shutdown', methods=['POST']) + def shutdown(): + self.shutting_down = True + return _('Closing...') + '

' + _('It is safe to close this window now.') + @self.app.route('/resources/', methods=['GET']) def resources(resource): return send_from_directory(self.resources_path, resource, max_age=1) + @self.app.route('/ping') + def ping(): + debug.log("got a ping") + # Javascript is letting us know it's still there. This resets self.last_request_time. + return "pong" + @self.app.route('/settings/', methods=['POST']) def set_field(field_name): self.metadata[field_name] = request.json['value'] @@ -155,10 +215,40 @@ class PrintPreviewServer(Thread): def get_realistic_overview(): return Response(self.realistic_overview_svg, mimetype='image/svg+xml') + @self.app.route('/printing/start') + def printing_start(): + # temporarily turn off the watcher while the print dialog is up, + # because javascript will be frozen + self.last_request_time = None + return "OK" + + @self.app.route('/printing/end') + def printing_end(): + # nothing to do here -- request_started() will restart the watcher + return "OK" + def stop(self): self.flask_server.shutdown() self.server_thread.join() + def watch(self): + try: + while True: + time.sleep(1) + if self.shutting_down: + debug.log("watcher thread: shutting down") + self.stop() + break + + if self.last_request_time is not None and (time.time() - self.last_request_time) > 3: + debug.log("watcher thread: timed out, stopping") + self.stop() + break + except BaseException: + # seems like sometimes this thread blows up during shutdown + debug.log(f"exception in watcher {sys.exc_info()}") + pass + def disable_logging(self): logging.getLogger('werkzeug').setLevel(logging.ERROR) @@ -180,6 +270,52 @@ class PrintPreviewServer(Thread): self.flask_server = make_server(self.host, self.port, self.app) self.server_thread = Thread(target=self.flask_server.serve_forever) self.server_thread.start() + self.started = True + self.server_thread.join() + + +class PrintInfoFrame(wx.Frame): + def __init__(self, *args, **kwargs): + self.print_server = kwargs.pop("print_server") + wx.Frame.__init__(self, *args, **kwargs) + + panel = wx.Panel(self) + sizer = wx.BoxSizer(wx.VERTICAL) + + self.message = _( + "A print preview has been opened in your web browser. " + "This window will stay open in order to communicate with the JavaScript code running in your browser.\n\n" + "This window will close after you close the print preview in your browser, or you can close it manually if necessary." + ) + self.text = wx.StaticText(panel, label=self.message) + font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.NORMAL) + self.text.SetFont(font) + self.Bind(wx.EVT_SIZE, self.resize) + sizer.Add(self.text, proportion=1, flag=wx.ALL | wx.EXPAND, border=20) + + stop_button = wx.Button(panel, id=wx.ID_CLOSE) + stop_button.Bind(wx.EVT_BUTTON, self.close_button_clicked) + sizer.Add(stop_button, proportion=0, flag=wx.ALIGN_CENTER | wx.ALL, border=10) + + panel.SetSizer(sizer) + panel.Layout() + + self.timer = wx.PyTimer(self.__watcher) + self.timer.Start(250) + + def resize(self, event=None): + self.text.SetLabel(self.message) + self.text.Wrap(self.GetSize().width - 35) + self.Layout() + + def close_button_clicked(self, event): + self.print_server.stop() + + def __watcher(self): + if self.print_server.started and not self.print_server.is_alive(): + self.timer.Stop() + self.timer = None + self.Destroy() class Print(InkstitchExtension): @@ -330,12 +466,11 @@ class Print(InkstitchExtension): realistic_color_block_svgs=realistic_color_block_svgs ) print_server.start() - # Wait for print_server.host and print_server.port to be populated. - # Hacky, but Flask doesn't have an option for a callback to be run - # after startup. - time.sleep(0.5) - browser_window = open_url(print_server.host, print_server.port, True) - browser_window.wait() - print_server.stop() - print_server.join() + time.sleep(1) + open_url("http://%s:%s/" % (print_server.host, print_server.port)) + + app = wx.App() + info_frame = PrintInfoFrame(None, title=_("Ink/Stitch Print"), size=(450, 350), print_server=print_server) + info_frame.Show() + app.MainLoop() diff --git a/print/resources/inkstitch.js b/print/resources/inkstitch.js index 382ced3bd..540ed7f0a 100644 --- a/print/resources/inkstitch.js +++ b/print/resources/inkstitch.js @@ -11,6 +11,12 @@ var realistic_rendering = {}; var realistic_cache = {}; var normal_rendering = {}; +function ping() { + $.get("/ping") + .done(function() { setTimeout(ping, 1000) }) + .fail(function() { $('#errors').attr('class', 'show') }); +} + //function to chunk opd view into pieces // source: https://stackoverflow.com/questions/3366529/wrap-every-3-divs-in-a-div $.fn.chunk = function(size) { @@ -203,6 +209,7 @@ function setSVGTransform(figure, transform) { } $(function() { + setTimeout(ping, 1000); /* SCALING AND MOVING SVG */ /* Mousewheel scaling */ @@ -372,20 +379,35 @@ $(function() { /* Settings Bar */ $('button.close').click(function() { - window.close(); + $.post('/shutdown', {}) + .always(function(data) { + window.close(); + + /* Chrome and Firefox both have a rule: scripts can only close windows + * that they opened. Chrome seems to have an exception for windows that + * were opened by an outside program, so the above works fine. Firefox + * steadfastly refuses to allow us to close the window, so we'll tell + * the user (in their language) that they can close it. + */ + setTimeout(function() { + document.open(); + document.write("" + data + ""); + document.close(); + }, 1000); + }); }); $('button.print').click(function() { - var pageSize = $('select#printing-size').find(':selected').text(); - window.inkstitchAPI.openpdf(pageSize) + // printing halts all javascript activity, so we need to tell the backend + // not to shut down until we're done. + $.get("/printing/start") + .done(function() { + window.print(); + $.get("/printing/end"); + }); }); - $('button.save-pdf').click(function() { - var pageSize = $('select#printing-size').find(':selected').text(); - window.inkstitchAPI.savepdf(pageSize) - }); - - $('button.settings').click(function(){ +$('button.settings').click(function(){ $('#settings-ui').show(); }); diff --git a/print/resources/style.css b/print/resources/style.css index db5ba603f..a32007444 100644 --- a/print/resources/style.css +++ b/print/resources/style.css @@ -125,7 +125,7 @@ body { color: white; } - .ui button.print, .ui button.save-pdf { + .ui button.print { border: 1px solid rgb(50,132,50); } diff --git a/print/templates/ui.html b/print/templates/ui.html index 4282838d3..846b7b497 100644 --- a/print/templates/ui.html +++ b/print/templates/ui.html @@ -2,7 +2,6 @@

{{ _('Ink/Stitch Print Preview') }}

-