Commit ddfd0b4f authored by Jonas Hahnfeld's avatar Jonas Hahnfeld

Issue 5949: Add option to use Ghostscript API instead of forking

This is much more efficient than the current scheme when converting
many PostScript files, for example when building the documentation.
For the Notation Reference, lilypond-book now takes ~2m30s instead
of 5m to compile all snippets from the notation.tely file. Other
manuals benefit less, but still the time for 'make doc' on my system
improves by one third from around 33m to 22m.

An alternative would have been to always call gsapi_init_with_args
with the same arguments used until now. This indeed works and avoids
the overhead of forking, but the instance cannot be reused. Calling
gsapi_new_instance -> gsapi_init_with_args -> gsapi_delete_instance
for every conversion still means a lot of overhead and a prototype
suggested only a small gain compared to the previous solution.

This is not the default because recent versions of Ghostscript are
distributed under the AGPL and it's unclear what the implications of
linking to the library is. If built with support for the API, use
-dgs-api=#f to still fork the gs command.
parent 8343ec88
......@@ -8,6 +8,9 @@
/* default lilypond locale dir */
#define LOCALEDIR "@LOCALEDIR@"
/* define to use the Ghostscript API */
#define GS_API 0
/* define if you have chroot */
#define HAVE_CHROOT 0
......
......@@ -28,6 +28,7 @@ FREETYPE2_CFLAGS = @FREETYPE2_CFLAGS@
FREETYPE2_LIBS = @FREETYPE2_LIBS@
GLIB_CLFAGS = @GLIB_CFLAGS@ @GOBJECT_CFLAGS@
GLIB_LIBS = @GLIB_LIBS@ @GOBJECT_LIBS@
GS_API = @GS_API@
GS920 = @GS920@
GUILE = @GUILE@
GUILE_CFLAGS = @GUILE_CFLAGS@
......
......@@ -346,8 +346,29 @@ else
DOCUMENTATION_REQUIRED=OPTIONAL
fi
# GhostScript
STEPMAKE_PATH_PROG(GHOSTSCRIPT, gs, $DOCUMENTATION_REQUIRED, 8.60)
GS_API=no
AC_ARG_ENABLE(gs-api,
[AS_HELP_STRING([--enable-gs-api],
[Link to libgs and use Ghostscript API instead of invoking]
[the executable. Beware of licensing implications!]
[Default: off])],
[GS_API=$enableval])
AC_SUBST(GS_API)
if test "$GS_API" = "yes"; then
AC_CHECK_HEADER([ghostscript/iapi.h], [
AC_CHECK_LIB([gs], [gsapi_new_instance], [GHOSTSCRIPT_FOUND="yes"],
[GHOSTSCRIPT_FOUND="no"])
], [GHOSTSCRIPT_FOUND="no"])
if test "$GHOSTSCRIPT_FOUND" = "no"; then
STEPMAKE_ADD_ENTRY(REQUIRED, ["libgs-dev"])
fi
AC_DEFINE(GS_API)
fi
AC_SUBST(GS920)
AC_SUBST(EXTRACTPDFMARK)
AC_SUBST(USE_EXTRACTPDFMARK)
......
......@@ -40,6 +40,9 @@ $(EXECUTABLE): $(O_FILES) $(outdir)/version.hh $(FLOWER_LIB)
$(CXX) $(ALL_CXXFLAGS) -o $@ $(O_FILES) $(LDLIBS) $(ALL_LDFLAGS)
ifeq ($(GS_API),yes)
MODULE_LDFLAGS += -lgs
endif
ifeq ($(LINK_GXX_STATICALLY),yes)
MODULE_LDFLAGS += -L$(outdir) -static-libgcc
endif
......
......@@ -33,6 +33,11 @@
#include "version.hh"
#include "warn.hh"
#if GS_API
#include <ghostscript/iapi.h>
#include <ghostscript/ierrors.h>
#endif
#include <cstdio>
#include <cstring> /* memset */
#include <ctype.h>
......@@ -673,3 +678,127 @@ LY_DEFINE (ly_spawn, "ly:spawn",
return scm_from_int (exit_status);
}
#if GS_API
static void *gs_inst = NULL;
static string gs_args;
LY_DEFINE (ly_shutdown_gs, "ly:shutdown-gs", 0, 0, 0, (),
"Shutdown GhostScript instance and flush pending writes.")
{
if (gs_inst == NULL)
{
assert (gs_args.length () == 0);
return SCM_UNDEFINED;
}
debug_output (_ ("Exiting current GhostScript instance...\n"));
int exit_code;
gsapi_run_string (gs_inst, "quit", 0, &exit_code);
gsapi_exit (gs_inst);
gsapi_delete_instance (gs_inst);
gs_inst = NULL;
gs_args = "";
return SCM_UNDEFINED;
}
LY_DEFINE (ly_gs, "ly:gs", 5, 0, 0,
(SCM args, SCM device, SCM device_args, SCM input, SCM output),
"Use GhostScript started with @var{args} to convert @var{input}"
" into @var{output} using @var{device} with @var{device_args}.")
{
LY_ASSERT_TYPE (scm_is_pair, args, 1);
LY_ASSERT_TYPE (scm_is_string, device, 2);
LY_ASSERT_TYPE (scm_is_string, device_args, 3);
LY_ASSERT_TYPE (scm_is_string, input, 4);
LY_ASSERT_TYPE (scm_is_string, output, 5);
// Construct vector of arguments with default / mandatory ones filled in.
// gsapi_init_with_args wants modifiable strings, so create local variables
// with copies of the content.
// (The first argument is the "program name" and is ignored.)
char gs[] = "gs";
char q[] = "-q";
char nodisplay[] = "-dNODISPLAY";
char nosafer[] = "-dNOSAFER";
char nopause[] = "-dNOPAUSE";
vector<char *> argv { gs, q, nodisplay, nosafer, nopause };
// Additionally keep track of converted strings to free them afterwards.
vector<char *> passed_args;
// Ensure that the string of arguments is never empty.
string new_args(" ");
for (SCM s = args; scm_is_pair (s); s = scm_cdr (s))
{
char *a = ly_scm2str0 (scm_car (s));
argv.push_back (a);
passed_args.push_back (a);
new_args += string(a) + " ";
}
if (gs_args.length () > 0)
{
assert (gs_inst != NULL);
if (gs_args != new_args)
{
debug_output (_ ("Mismatch of GhostScript arguments!\n"));
ly_shutdown_gs ();
}
}
if (gs_inst == NULL)
{
debug_output (_f ("Starting GhostScript instance with arguments: %s\n",
new_args.c_str ()));
// Save current string of arguments to later compare if we need a new
// instance with different parameters.
gs_args = new_args;
int code = gsapi_new_instance (&gs_inst, NULL);
if (code == 0)
code = gsapi_set_arg_encoding (gs_inst, GS_ARG_ENCODING_UTF8);
int argc = static_cast<int>(argv.size ());
if (code == 0)
code = gsapi_init_with_args (gs_inst, argc, argv.data ());
// Handle errors from above calls.
if (code < 0)
{
warning (_ ("Could not start GhostScript instance!"));
scm_throw (ly_symbol2scm ("ly-file-failed"),
scm_list_1 (output));
return SCM_UNDEFINED;
}
}
// Free all converted strings.
for (char *a : passed_args)
free (a);
// Construct the command.
string command = "mark ";
command += "/OutputFile (" + ly_scm2string (output) + ") ";
command += ly_scm2string (device_args) + " ";
command += "(" + ly_scm2string (device) + ") finddevice ";
command += "putdeviceprops setdevice ";
command += "(" + ly_scm2string (input) + ") run";
debug_output (_f ("Running GhostScript command: %s\n", command.c_str ()));
int exit_code;
int code = gsapi_run_string (gs_inst, command.c_str (), 0, &exit_code);
// gs_error_invalidexit could be avoided by having a 'quit' at the end of
// the command. However this leads to execstackoverflow when running many
// conversions, for example when compiling the Notation Reference.
if (code != 0 && code != gs_error_Quit && code != gs_error_invalidexit)
{
warning (_ ("Error when running GhostScript command!"));
scm_throw (ly_symbol2scm ("ly-file-failed"),
scm_list_1 (output));
}
return SCM_UNDEFINED;
}
#endif
......@@ -52,6 +52,10 @@
#include "warn.hh"
#include "lily-imports.hh"
#if GS_API
#include <ghostscript/iapi.h>
#endif
#include <cassert>
#include <cerrno>
#include <clocale>
......@@ -335,6 +339,26 @@ warranty ()
copyright ();
printf ("\n");
printf ("%s", (_ (WARRANTY).c_str ()));
#if GS_API
printf ("\n");
printf ("%s", (_ ("linked against Ghostscript:").c_str ()));
printf ("\n");
void *gs_inst = NULL;
gsapi_new_instance (&gs_inst, NULL);
gsapi_set_arg_encoding (gs_inst, GS_ARG_ENCODING_UTF8);
// gsapi_init_with_args wants modifiable strings, so create local variables
// with copies of the content.
// (The first argument is the "program name" and is ignored.)
char gs[] = "gs";
char nodisplay[] = "-dNODISPLAY";
char *argv[] = { gs, nodisplay };
gsapi_init_with_args (gs_inst, 2, argv);
gsapi_exit (gs_inst);
gsapi_delete_instance (gs_inst);
#endif
}
static void
......@@ -871,6 +895,12 @@ main (int argc, char **argv, char **envp)
setup_paths (argv[0]);
setup_guile_env (); // set up environment variables to pass into Guile API
#if !GS_API
// Let Guile know whether the Ghostscript API is not available.
init_scheme_variables_global += "(cons \'gs-api '#f)\n";
#endif
/*
* Start up Guile API using main_with_guile as a callback.
*/
......
......@@ -75,7 +75,7 @@
base-name tmp-name is-eps)
(let* ((pdf-name (string-append base-name ".pdf"))
(*unspecified* (if #f #f))
(cmd
(gs-cmd
(remove (lambda (x) (eq? x *unspecified*))
(list
(search-gs)
......@@ -85,29 +85,31 @@
(eq? PLATFORM 'windows))
"-dNOSAFER"
"-dSAFER")
"-dNOPAUSE"
"-dBATCH")))
(args
(remove (lambda (x) (eq? x *unspecified*))
(list
(if is-eps
"-dEPSCrop"
(ly:format "-dDEVICEWIDTHPOINTS=~$" paper-width))
(if is-eps
*unspecified*
(ly:format "-dDEVICEHEIGHTPOINTS=~$" paper-height))
"-dCompatibilityLevel=1.4"
"-dNOPAUSE"
"-dBATCH"
"-r1200"
"-sDEVICE=pdfwrite"
"-dAutoRotatePages=/None"
"-dPrinted=false"
(string-append "-sOutputFile="
(string-join
(string-split pdf-name #\%)
"%%"))
"-c.setpdfwrite"
(string-append "-f" tmp-name)))))
"-dPrinted=false")))
(output-file (string-join (string-split pdf-name #\%) "%%"))
(gs-cmd-output
(list
"-sDEVICE=pdfwrite"
(string-append "-sOutputFile=" output-file)
"-c.setpdfwrite"
(string-append "-f" tmp-name))))
(ly:message (_ "Converting to `~a'...\n") pdf-name)
(ly:system cmd)))
(if (ly:get-option 'gs-api)
(ly:gs args "pdfwrite" "" tmp-name output-file)
(ly:system (append gs-cmd (append args gs-cmd-output))))))
(define-public (postscript->png resolution paper-width paper-height
base-name tmp-name is-eps)
......
......@@ -292,6 +292,9 @@ given amount (in mm).")
(font-ps-resdir
#f
"Build a subset of PostScript resource directory for embedding fonts.")
(gs-api
#t
"Whether to use the Ghostscript API (read-only if not available).")
(gs-load-fonts
#f
"Load fonts via Ghostscript.")
......@@ -935,6 +938,8 @@ PIDs or the number of the process."
(define* (ly:exit status #:optional (silently #f))
"Exit function for lilypond"
(if (ly:get-option 'gs-api)
(ly:shutdown-gs))
(if (not silently)
(case status
((0) (ly:basic-progress (_ "Success: compilation successfully completed")))
......
......@@ -123,7 +123,7 @@
(output-file (if multi-page? pngn-gs png1-gs))
(*unspecified* (if #f #f))
(cmd
(gs-cmd
(remove (lambda (x) (eq? x *unspecified*))
(list
(search-gs)
......@@ -133,26 +133,37 @@
(eq? PLATFORM 'windows))
"-dNOSAFER"
"-dSAFER")
"-dNOPAUSE"
"-dBATCH")))
(args
(remove (lambda (x) (eq? x *unspecified*))
(list
(if is-eps
"-dEPSCrop"
(ly:format "-dDEVICEWIDTHPOINTS=~$" page-width))
(if is-eps
*unspecified*
(ly:format "-dDEVICEHEIGHTPOINTS=~$" page-height))
"-dGraphicsAlphaBits=4"
"-dTextAlphaBits=4"
"-dNOPAUSE"
"-dBATCH"
(ly:format "-sDEVICE=~a" pixmap-format)
"-dAutoRotatePages=/None"
"-dPrinted=false"
(string-append "-sOutputFile=" output-file)
(ly:format "-r~a" (* anti-alias-factor resolution))
(string-append "-f" tmp-name))))
"-dPrinted=false")))
(alpha-args "/GraphicsAlphaBits 4 /TextAlphaBits 4")
(hw-resolution (* anti-alias-factor resolution))
(hw-resolution-arg
(ly:format "/HWResolution [~a ~a]" hw-resolution hw-resolution))
(device-args (string-append alpha-args " " hw-resolution-arg))
(gs-cmd-output
(list
"-dGraphicsAlphaBits=4"
"-dTextAlphaBits=4"
(ly:format "-r~a" hw-resolution)
(ly:format "-sDEVICE=~a" pixmap-format)
(string-append "-sOutputFile=" output-file)
(string-append "-f" tmp-name)))
(files '()))
(ly:system cmd)
(if (ly:get-option 'gs-api)
(ly:gs args pixmap-format device-args tmp-name output-file)
(ly:system (append gs-cmd (append args gs-cmd-output))))
(set! files
(if multi-page?
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment