You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
507 lines
14 KiB
507 lines
14 KiB
#include "LatexController.h" |
|
|
|
#include "Control.h" |
|
|
|
#include "gui/XournalView.h" |
|
#include "gui/dialog/LatexDialog.h" |
|
#include "undo/InsertUndoAction.h" |
|
|
|
#include <i18n.h> |
|
#include <Util.h> |
|
#include <Stacktrace.h> |
|
#include <XojMsgBox.h> |
|
#include <StringUtils.h> |
|
|
|
#include "pixbuf-utils.h" |
|
|
|
/** |
|
* First half of the LaTeX template used to generate preview PDFs. User-supplied |
|
* formulas will be inserted between the two halves. |
|
* |
|
* This template is necessarily complicated because we need to cause an error if |
|
* the rendered formula is blank. Otherwise, a completely blank, sizeless PDF |
|
* will be generated, which Poppler will be unable to load. |
|
*/ |
|
const char* LATEX_TEMPLATE_1 = |
|
R"(\documentclass[crop, border=5pt]{standalone})" "\n" |
|
R"(\usepackage{amsmath})" "\n" |
|
R"(\usepackage{amssymb})" "\n" |
|
R"(\usepackage{ifthen})" "\n" |
|
R"(\newlength{\pheight})" "\n" |
|
R"(\def\preview{\(\displaystyle)" "\n"; |
|
|
|
const char* LATEX_TEMPLATE_2 = |
|
"\n\\)}\n" |
|
R"(\begin{document})" "\n" |
|
R"(\settoheight{\pheight}{\preview} %)" "\n" |
|
R"(\ifthenelse{\pheight=0})" "\n" |
|
R"({\GenericError{}{xournalpp: blank formula}{}{}})" "\n" |
|
R"(\preview)" "\n" |
|
R"(\end{document})" "\n"; |
|
|
|
LatexController::LatexController(Control* control) |
|
: control(control), |
|
dlg(control->getGladeSearchPath()), |
|
doc(control->getDocument()), |
|
texTmpDir(Util::getTmpDirSubfolder("tex")) |
|
{ |
|
XOJ_INIT_TYPE(LatexController); |
|
Util::ensureFolderExists(this->texTmpDir); |
|
} |
|
|
|
LatexController::~LatexController() |
|
{ |
|
XOJ_CHECK_TYPE(LatexController); |
|
|
|
this->control = NULL; |
|
|
|
XOJ_RELEASE_TYPE(LatexController); |
|
} |
|
|
|
/** |
|
* Find the tex executable, return false if not found |
|
*/ |
|
LatexController::FindDependencyStatus LatexController::findTexDependencies() |
|
{ |
|
XOJ_CHECK_TYPE(LatexController); |
|
|
|
gchar* pdflatex = g_find_program_in_path("pdflatex"); |
|
if (!pdflatex) |
|
{ |
|
string msg = _("Could not find pdflatex in PATH.\nPlease install pdflatex first and make sure it's in the PATH."); |
|
return LatexController::FindDependencyStatus(false, msg); |
|
} |
|
this->pdflatexPath = pdflatex; |
|
g_free(pdflatex); |
|
|
|
// Check for 'standalone' latex package |
|
static gchar* kpsewhichArgs[] = { g_strdup("kpsewhich"), g_strdup("standalone") }; |
|
auto kpsewhichFlags = GSpawnFlags(G_SPAWN_DEFAULT | G_SPAWN_SEARCH_PATH | G_SPAWN_STDOUT_TO_DEV_NULL); |
|
GError* kpsewhichErr = nullptr; |
|
gint kpsewhichStatus; |
|
g_spawn_sync( |
|
nullptr, kpsewhichArgs, nullptr, kpsewhichFlags, nullptr, nullptr, nullptr, |
|
nullptr, &kpsewhichStatus, &kpsewhichErr); |
|
if (kpsewhichErr != nullptr) |
|
{ |
|
g_error_free(kpsewhichErr); |
|
string msg = _("Could not find kpsewhich in PATH; please install kpsewhich and put it on path."); |
|
return LatexController::FindDependencyStatus(false, msg); |
|
} |
|
else if (kpsewhichStatus != 0) |
|
{ |
|
string msg = FS(_F("Could not find the LaTeX package 'standalone'.\nPlease install standalone and make sure it's accessible by your LaTeX installation.")); |
|
return LatexController::FindDependencyStatus(false, msg); |
|
} |
|
|
|
return LatexController::FindDependencyStatus(true, ""); |
|
} |
|
|
|
std::unique_ptr<GPid> LatexController::runCommandAsync(string texString) |
|
{ |
|
XOJ_CHECK_TYPE(LatexController); |
|
g_assert(!this->isUpdating); |
|
|
|
string texContents = LATEX_TEMPLATE_1; |
|
texContents += texString; |
|
texContents += LATEX_TEMPLATE_2; |
|
|
|
Path texFile = this->texTmpDir / "tex.tex"; |
|
|
|
GError* err = NULL; |
|
if (!g_file_set_contents(texFile.c_str(), texContents.c_str(), texContents.length(), &err)) |
|
{ |
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), FS(_F("Could not save .tex file: {1}") % err->message)); |
|
g_error_free(err); |
|
return nullptr; |
|
} |
|
|
|
char* texFileEscaped = g_strescape(texFile.c_str(), NULL); |
|
char* cmd = g_strdup(this->pdflatexPath.c_str()); |
|
|
|
static char* texFlag = g_strdup("-interaction=nonstopmode"); |
|
char* argv[] = { cmd, texFlag, texFileEscaped, NULL }; |
|
|
|
std::unique_ptr<GPid> pdflatexPid(new GPid); |
|
GSpawnFlags flags = GSpawnFlags(G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL | G_SPAWN_DO_NOT_REAP_CHILD); |
|
|
|
this->setUpdating(true); |
|
this->lastPreviewedTex = texString; |
|
|
|
bool success = g_spawn_async(texTmpDir.c_str(), argv, nullptr, flags, nullptr, nullptr, pdflatexPid.get(), &err); |
|
if (!success) |
|
{ |
|
string message = FS(_F("Could not start pdflatex: {1} (exit code: {2})") % err->message % err->code); |
|
g_warning("%s", message.c_str()); |
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), message); |
|
|
|
g_error_free(err); |
|
this->setUpdating(false); |
|
pdflatexPid.reset(); |
|
} |
|
|
|
g_free(texFileEscaped); |
|
g_free(cmd); |
|
|
|
return pdflatexPid; |
|
} |
|
|
|
/** |
|
* Find a selected tex element, and load it |
|
*/ |
|
void LatexController::findSelectedTexElement() |
|
{ |
|
XOJ_CHECK_TYPE(LatexController); |
|
|
|
this->doc->lock(); |
|
int pageNr = this->control->getCurrentPageNo(); |
|
if (pageNr == -1) |
|
{ |
|
this->doc->unlock(); |
|
return; |
|
} |
|
this->view = this->control->getWindow()->getXournal()->getViewFor(pageNr); |
|
if (view == NULL) |
|
{ |
|
this->doc->unlock(); |
|
return; |
|
} |
|
|
|
// we get the selection |
|
this->page = this->doc->getPage(pageNr); |
|
this->layer = page->getSelectedLayer(); |
|
|
|
this->selectedTexImage = view->getSelectedTex(); |
|
this->selectedText = view->getSelectedText(); |
|
|
|
if (this->selectedTexImage || this->selectedText) |
|
{ |
|
// this will get the position of the Latex properly |
|
EditSelection* theSelection = control->getWindow()->getXournal()->getSelection(); |
|
this->posx = theSelection->getXOnView(); |
|
this->posy = theSelection->getYOnView(); |
|
|
|
if (this->selectedTexImage != nullptr) |
|
{ |
|
this->initialTex = this->selectedTexImage->getText(); |
|
this->imgwidth = this->selectedTexImage->getElementWidth(); |
|
this->imgheight = this->selectedTexImage->getElementHeight(); |
|
} |
|
else |
|
{ |
|
this->initialTex += "\\text{"; |
|
this->initialTex += this->selectedText->getText(); |
|
this->initialTex += "}"; |
|
this->imgwidth = this->selectedText->getElementWidth(); |
|
this->imgheight = this->selectedText->getElementHeight(); |
|
} |
|
} |
|
else |
|
{ |
|
// This is a new latex object, so here we pick a convenient initial location |
|
const double zoom = this->control->getWindow()->getXournal()->getZoom(); |
|
Layout* const layout = this->control->getWindow()->getLayout(); |
|
|
|
// Calculate coordinates (screen) of the center of the visible area |
|
const auto visibleBounds = layout->getVisibleRect(); |
|
const double centerX = visibleBounds.x + 0.5 * visibleBounds.width; |
|
const double centerY = visibleBounds.y + 0.5 * visibleBounds.height; |
|
|
|
if (layout->getViewAt(centerX, centerY) == this->view) |
|
{ |
|
// Pick the center of the visible area (converting from screen to page coordinates) |
|
this->posx = (centerX - this->view->getX()) / zoom; |
|
this->posy = (centerY - this->view->getY()) / zoom; |
|
} |
|
else |
|
{ |
|
// No better location, so just center it on the page (possibly out of viewport) |
|
this->posx = 0.5 * this->page->getWidth(); |
|
this->posy = 0.5 * this->page->getHeight(); |
|
} |
|
} |
|
this->doc->unlock(); |
|
|
|
// need to do this otherwise we can't remove the image for its replacement |
|
this->control->clearSelectionEndText(); |
|
} |
|
|
|
string LatexController::showTexEditDialog() |
|
{ |
|
XOJ_CHECK_TYPE(LatexController); |
|
|
|
// Attach the signal handler before setting the buffer text so that the |
|
// callback is triggered |
|
gulong signalHandler = g_signal_connect(dlg.getTextBuffer(), "changed", G_CALLBACK(handleTexChanged), this); |
|
bool isNewFormula = this->initialTex.empty(); |
|
this->dlg.setFinalTex(isNewFormula ? "x^2" : this->initialTex); |
|
|
|
if (this->temporaryRender != nullptr) |
|
{ |
|
this->dlg.setTempRender(this->temporaryRender->getPdf()); |
|
} |
|
|
|
this->dlg.show(GTK_WINDOW(control->getWindow()->getWindow()), isNewFormula); |
|
g_signal_handler_disconnect(dlg.getTextBuffer(), signalHandler); |
|
|
|
string result = this->dlg.getFinalTex(); |
|
// If the user cancelled, there is no change in the latex string. |
|
result = result == "" ? initialTex : result; |
|
return result; |
|
} |
|
|
|
void LatexController::triggerImageUpdate(string texString) |
|
{ |
|
if (this->isUpdating) |
|
{ |
|
return; |
|
} |
|
|
|
std::unique_ptr<GPid> pid = this->runCommandAsync(texString); |
|
if (pid != nullptr) |
|
{ |
|
g_assert(this->isUpdating); |
|
g_child_watch_add(*pid, reinterpret_cast<GChildWatchFunc>(onPdfRenderComplete), this); |
|
} |
|
} |
|
|
|
/** |
|
* Text-changed handler: when the Buffer in the dialog changes, this handler |
|
* removes the previous existing render and creates a new one. We need to do it |
|
* through 'self' because signal handlers cannot directly access non-static |
|
* methods and non-static fields such as 'dlg' so we need to wrap all the dlg |
|
* method inside small methods in 'self'. To improve performance, we render the |
|
* text asynchronously. |
|
*/ |
|
void LatexController::handleTexChanged(GtkTextBuffer* buffer, LatexController* self) |
|
{ |
|
XOJ_CHECK_TYPE_OBJ(self, LatexController); |
|
self->triggerImageUpdate(self->dlg.getBufferContents()); |
|
} |
|
|
|
void LatexController::onPdfRenderComplete(GPid pid, gint returnCode, LatexController* self) |
|
{ |
|
XOJ_CHECK_TYPE_OBJ(self, LatexController); |
|
g_assert(self->isUpdating); |
|
GError* err = nullptr; |
|
g_spawn_check_exit_status(returnCode, &err); |
|
g_spawn_close_pid(pid); |
|
string currentTex = self->dlg.getBufferContents(); |
|
bool shouldUpdate = self->lastPreviewedTex != currentTex; |
|
if (err != nullptr) |
|
{ |
|
self->isValidTex = false; |
|
if (!g_error_matches(err, G_SPAWN_EXIT_ERROR, 1)) |
|
{ |
|
// The error was not caused by invalid LaTeX. |
|
string message = FS(_F("pdflatex encountered an error: {1} (exit code: {2})") % err->message % err->code); |
|
g_warning("%s", message.c_str()); |
|
XojMsgBox::showErrorToUser(self->control->getGtkWindow(), message); |
|
} |
|
Path pdfPath = self->texTmpDir / "tex.pdf"; |
|
if (pdfPath.exists()) |
|
{ |
|
// Delete the pdf to prevent more errors |
|
pdfPath.deleteFile(); |
|
} |
|
g_error_free(err); |
|
} |
|
else |
|
{ |
|
self->isValidTex = true; |
|
self->temporaryRender = self->loadRendered(currentTex); |
|
if (self->temporaryRender != nullptr) |
|
{ |
|
self->dlg.setTempRender(self->temporaryRender->getPdf()); |
|
} |
|
|
|
} |
|
self->setUpdating(false); |
|
if (shouldUpdate) |
|
{ |
|
self->triggerImageUpdate(currentTex); |
|
} |
|
} |
|
|
|
void LatexController::setUpdating(bool newValue) |
|
{ |
|
XOJ_CHECK_TYPE(LatexController); |
|
GtkWidget* okButton = this->dlg.get("texokbutton"); |
|
bool buttonEnabled = true; |
|
if ((!this->isUpdating && newValue) || (this->isUpdating && !newValue)) |
|
{ |
|
// Disable LatexDialog OK button while updating. This is a workaround |
|
// for the fact that 1) the LatexController only lives while the dialog |
|
// is open; 2) the preview is generated asynchronously; and 3) the `run` |
|
// method that inserts the TexImage object is called synchronously after |
|
// the dialog is closed with the OK button. |
|
buttonEnabled = !newValue; |
|
} |
|
|
|
|
|
// Invalid LaTeX will generate an invalid PDF, so disable the OK button if |
|
// needed. |
|
buttonEnabled = buttonEnabled && this->isValidTex; |
|
|
|
gtk_widget_set_sensitive(okButton, buttonEnabled); |
|
|
|
GtkLabel* errorLabel = GTK_LABEL(this->dlg.get("texErrorLabel")); |
|
gtk_label_set_text(errorLabel, this->isValidTex ? "" : N_("The formula is empty when rendered or invalid.")); |
|
|
|
this->isUpdating = newValue; |
|
} |
|
|
|
void LatexController::deleteOldImage() |
|
{ |
|
XOJ_CHECK_TYPE(LatexController); |
|
|
|
if (this->selectedTexImage != nullptr) |
|
{ |
|
g_assert(this->selectedText == nullptr); |
|
EditSelection selection(control->getUndoRedoHandler(), selectedTexImage, view, page); |
|
this->view->getXournal()->deleteSelection(&selection); |
|
this->selectedTexImage = nullptr; |
|
} |
|
else if (this->selectedText) |
|
{ |
|
g_assert(this->selectedTexImage == nullptr); |
|
EditSelection selection(control->getUndoRedoHandler(), selectedText, view, page); |
|
view->getXournal()->deleteSelection(&selection); |
|
this->selectedText = nullptr; |
|
} |
|
} |
|
|
|
std::unique_ptr<TexImage> LatexController::convertDocumentToImage(PopplerDocument* doc, string formula) |
|
{ |
|
XOJ_CHECK_TYPE(LatexController); |
|
|
|
if (poppler_document_get_n_pages(doc) < 1) |
|
{ |
|
return NULL; |
|
} |
|
|
|
PopplerPage* page = poppler_document_get_page(doc, 0); |
|
|
|
|
|
double pageWidth = 0; |
|
double pageHeight = 0; |
|
poppler_page_get_size(page, &pageWidth, &pageHeight); |
|
|
|
std::unique_ptr<TexImage> img(new TexImage()); |
|
img->setX(posx); |
|
img->setY(posy); |
|
img->setText(formula); |
|
|
|
if (imgheight) |
|
{ |
|
double ratio = pageWidth / pageHeight; |
|
if (ratio == 0) |
|
{ |
|
img->setWidth(imgwidth == 0 ? 10 : imgwidth); |
|
} |
|
else |
|
{ |
|
img->setWidth(imgheight * ratio); |
|
} |
|
img->setHeight(imgheight); |
|
} |
|
else |
|
{ |
|
img->setWidth(pageWidth); |
|
img->setHeight(pageHeight); |
|
} |
|
|
|
return img; |
|
} |
|
|
|
std::unique_ptr<TexImage> LatexController::loadRendered(string renderedTex) |
|
{ |
|
XOJ_CHECK_TYPE(LatexController); |
|
|
|
if (!this->isValidTex) |
|
{ |
|
return nullptr; |
|
} |
|
|
|
Path pdfPath = texTmpDir / "tex.pdf"; |
|
GError* err = NULL; |
|
|
|
gchar* fileContents = NULL; |
|
gsize fileLength = 0; |
|
if (!g_file_get_contents(pdfPath.c_str(), &fileContents, &fileLength, &err)) |
|
{ |
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), |
|
FS(_F("Could not load LaTeX PDF file, File Error: {1}") % err->message)); |
|
g_error_free(err); |
|
return NULL; |
|
} |
|
|
|
PopplerDocument* pdf = poppler_document_new_from_data(fileContents, fileLength, NULL, &err); |
|
if (err != NULL) |
|
{ |
|
string message = FS(_F("Could not load LaTeX PDF file: {1}") % err->message); |
|
g_message("%s", message.c_str()); |
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), message); |
|
g_error_free(err); |
|
return NULL; |
|
} |
|
|
|
if (pdf == NULL) |
|
{ |
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), FS(_F("Could not load LaTeX PDF file"))); |
|
return NULL; |
|
} |
|
|
|
std::unique_ptr<TexImage> img = convertDocumentToImage(pdf, renderedTex); |
|
g_object_unref(pdf); |
|
|
|
// Do not assign the PDF, theoretical it should work, but it gets a Poppler PDF error |
|
// img->setPdf(pdf); |
|
img->setBinaryData(string(fileContents, fileLength)); |
|
|
|
g_free(fileContents); |
|
|
|
return img; |
|
} |
|
|
|
void LatexController::insertTexImage() |
|
{ |
|
XOJ_CHECK_TYPE(LatexController); |
|
|
|
g_assert(this->temporaryRender != nullptr); |
|
TexImage* img = this->temporaryRender.release(); |
|
|
|
this->deleteOldImage(); |
|
|
|
doc->lock(); |
|
layer->addElement(img); |
|
view->rerenderElement(img); |
|
doc->unlock(); |
|
|
|
control->getUndoRedoHandler()->addUndoAction(new InsertUndoAction(page, layer, img)); |
|
|
|
// Select element |
|
EditSelection* selection = new EditSelection(control->getUndoRedoHandler(), img, view, page); |
|
view->getXournal()->setSelection(selection); |
|
} |
|
|
|
void LatexController::run() |
|
{ |
|
XOJ_CHECK_TYPE(LatexController); |
|
|
|
auto depStatus = this->findTexDependencies(); |
|
if (!depStatus.success) |
|
{ |
|
XojMsgBox::showErrorToUser(control->getGtkWindow(), depStatus.errorMsg); |
|
return; |
|
} |
|
|
|
this->findSelectedTexElement(); |
|
string newTex = this->showTexEditDialog(); |
|
|
|
if (this->initialTex != newTex) |
|
{ |
|
g_assert(this->isValidTex); |
|
this->insertTexImage(); |
|
} |
|
}
|
|
|