// SPDX-License-Identifier: GPL-2.0-or-later /** \file * Rendering LaTeX file (pdf/eps/ps+latex output) * * The idea stems from GNUPlot's epslatex terminal output :-) */ /* * Authors: * Johan Engelen * Miklos Erdelyi * Jon A. Cruz * Abhishek Sharma * * Copyright (C) 2006-2011 Authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "latex-text-renderer.h" #include #include #include #include #include "libnrtype/Layout-TNG.h" #include <2geom/transforms.h> #include <2geom/rect.h> #include "object/sp-item.h" #include "object/sp-item-group.h" #include "object/sp-root.h" #include "object/sp-use.h" #include "object/sp-text.h" #include "object/sp-flowtext.h" #include "object/sp-rect.h" #include "style.h" #include "text-editing.h" #include "util/units.h" #include "extension/output.h" #include "extension/system.h" #include "inkscape-version.h" #include "io/sys.h" #include "document.h" namespace Inkscape { namespace Extension { namespace Internal { /** * This method is called by the PDF, EPS and PS output extensions. * @param filename This should be the filename without '_tex' extension to which the tex code should be written. Output goes to _tex, note the underscore instead of period. */ bool latex_render_document_text_to_file( SPDocument *doc, gchar const *filename, const gchar * const exportId, bool exportDrawing, bool exportCanvas, double bleedmargin_px, bool pdflatex) { doc->ensureUpToDate(); SPRoot *root = doc->getRoot(); SPItem *base = nullptr; bool pageBoundingBox = true; if (exportId && strcmp(exportId, "")) { // we want to export the given item only base = dynamic_cast(doc->getObjectById(exportId)); if (!base) { throw Inkscape::Extension::Output::export_id_not_found(exportId); } root->cropToObject(base); // TODO: This is inconsistent in CLI (should only happen for --export-id-only) pageBoundingBox = exportCanvas; } else { // we want to export the entire document from root base = root; pageBoundingBox = !exportDrawing; } if (!base) return false; /* Create renderer */ LaTeXTextRenderer *renderer = new LaTeXTextRenderer(pdflatex); bool ret = renderer->setTargetFile(filename); if (ret) { /* Render document */ bool ret = renderer->setupDocument(doc, pageBoundingBox, bleedmargin_px, base); if (ret) { renderer->renderItem(root); } } delete renderer; return ret; } LaTeXTextRenderer::LaTeXTextRenderer(bool pdflatex) : _stream(nullptr), _filename(nullptr), _pdflatex(pdflatex), _omittext_state(EMPTY), _omittext_page(1) { push_transform(Geom::identity()); } LaTeXTextRenderer::~LaTeXTextRenderer() { if (_stream) { writePostamble(); fclose(_stream); } /* restore default signal handling for SIGPIPE */ #if !defined(_WIN32) && !defined(__WIN32__) (void) signal(SIGPIPE, SIG_DFL); #endif if (_filename) { g_free(_filename); } return; } /** This should create the output LaTeX file, and assign it to _stream. * @return Returns true when successful */ bool LaTeXTextRenderer::setTargetFile(gchar const *filename) { if (filename != nullptr) { while (isspace(*filename)) filename += 1; _filename = g_path_get_basename(filename); gchar *filename_ext = g_strdup_printf("%s_tex", filename); Inkscape::IO::dump_fopen_call(filename_ext, "K"); FILE *osf = Inkscape::IO::fopen_utf8name(filename_ext, "w+"); if (!osf) { fprintf(stderr, "inkscape: fopen(%s): %s\n", filename_ext, strerror(errno)); g_free(filename_ext); return false; } _stream = osf; g_free(filename_ext); } /* fixme: this is kinda icky */ #if !defined(_WIN32) && !defined(__WIN32__) (void) signal(SIGPIPE, SIG_IGN); #endif fprintf(_stream, "%%%% Creator: Inkscape %s, www.inkscape.org\n", Inkscape::version_string); fprintf(_stream, "%%%% PDF/EPS/PS + LaTeX output extension by Johan Engelen, 2010\n"); fprintf(_stream, "%%%% Accompanies image file '%s' (pdf, eps, ps)\n", _filename); fprintf(_stream, "%%%%\n"); /* flush this to test output stream as early as possible */ if (fflush(_stream)) { if (ferror(_stream)) { g_print("Error %d on LaTeX file output stream: %s\n", errno, g_strerror(errno)); } g_print("Output to LaTeX file failed\n"); /* fixme: should use pclose() for pipes */ fclose(_stream); _stream = nullptr; fflush(stdout); return false; } writePreamble(); return true; } static char const preamble[] = "%% To include the image in your LaTeX document, write\n" "%% \\input{.pdf_tex}\n" "%% instead of\n" "%% \\includegraphics{.pdf}\n" "%% To scale the image, write\n" "%% \\def\\svgwidth{}\n" "%% \\input{.pdf_tex}\n" "%% instead of\n" "%% \\includegraphics[width=]{.pdf}\n" "%%\n" "%% Images with a different path to the parent latex file can\n" "%% be accessed with the `import' package (which may need to be\n" "%% installed) using\n" "%% \\usepackage{import}\n" "%% in the preamble, and then including the image with\n" "%% \\import{}{.pdf_tex}\n" "%% Alternatively, one can specify\n" "%% \\graphicspath{{/}}\n" "%% \n" "%% For more information, please see info/svg-inkscape on CTAN:\n" "%% http://tug.ctan.org/tex-archive/info/svg-inkscape\n" "%%\n" "\\begingroup%\n" " \\makeatletter%\n" " \\providecommand\\color[2][]{%\n" " \\errmessage{(Inkscape) Color is used for the text in Inkscape, but the package \'color.sty\' is not loaded}%\n" " \\renewcommand\\color[2][]{}%\n" " }%\n" " \\providecommand\\transparent[1]{%\n" " \\errmessage{(Inkscape) Transparency is used (non-zero) for the text in Inkscape, but the package \'transparent.sty\' is not loaded}%\n" " \\renewcommand\\transparent[1]{}%\n" " }%\n" " \\providecommand\\rotatebox[2]{#2}%\n" " \\newcommand*\\fsize{\\dimexpr\\f@size pt\\relax}%\n" " \\newcommand*\\lineheight[1]{\\fontsize{\\fsize}{#1\\fsize}\\selectfont}%\n"; static char const postamble[] = " \\end{picture}%\n" "\\endgroup%\n"; void LaTeXTextRenderer::writePreamble() { fprintf(_stream, "%s", preamble); } void LaTeXTextRenderer::writePostamble() { fprintf(_stream, "%s", postamble); } void LaTeXTextRenderer::sp_group_render(SPGroup *group) { std::vector l = (group->childList(false)); for(auto x : l){ SPItem *item = dynamic_cast(x); if (item) { renderItem(item); } } } void LaTeXTextRenderer::sp_use_render(SPUse *use) { bool translated = false; if ((use->x._set && use->x.computed != 0) || (use->y._set && use->y.computed != 0)) { Geom::Affine tp(Geom::Translate(use->x.computed, use->y.computed)); push_transform(tp); translated = true; } SPItem *childItem = dynamic_cast(use->child); if (childItem) { renderItem(childItem); } if (translated) { pop_transform(); } } void LaTeXTextRenderer::sp_text_render(SPText *textobj) { // Nothing to do here... (so don't emit an empty box) // Also avoids falling out of sync with the CairoRenderer (which won't render anything in this case either) if (textobj->layout.getActualLength() == 0) return; // Only PDFLaTeX supports importing a single page of a graphics file, // so only PDF backend gets interleaved text/graphics if (_pdflatex && _omittext_state == GRAPHIC_ON_TOP) _omittext_state = NEW_PAGE_ON_GRAPHIC; SPStyle *style = textobj->style; // get position and alignment // Align vertically on the baseline of the font (retrieved from the anchor point) // Align horizontally on anchorpoint gchar const *alignment = nullptr; gchar const *aligntabular = nullptr; switch (style->text_anchor.computed) { case SP_CSS_TEXT_ANCHOR_START: alignment = "[lt]"; aligntabular = "{l}"; break; case SP_CSS_TEXT_ANCHOR_END: alignment = "[rt]"; aligntabular = "{r}"; break; case SP_CSS_TEXT_ANCHOR_MIDDLE: default: alignment = "[t]"; aligntabular = "{c}"; break; } Geom::Point anchor; const auto baseline_anchor_point = textobj->layout.baselineAnchorPoint(); if (baseline_anchor_point) { anchor = (*baseline_anchor_point) * transform(); } else { g_warning("LaTeXTextRenderer::sp_text_render: baselineAnchorPoint unset, text position will be wrong. Please report the issue."); } // determine color and transparency (for now, use rgb color model as it is most native to Inkscape) bool has_color = false; // if the item has no color set, don't force black color bool has_transparency = false; // TODO: how to handle ICC colors? // give priority to fill color guint32 rgba = 0; float opacity = SP_SCALE24_TO_FLOAT(style->opacity.value); if (style->fill.set && style->fill.isColor()) { has_color = true; rgba = style->fill.value.color.toRGBA32(1.); opacity *= SP_SCALE24_TO_FLOAT(style->fill_opacity.value); } else if (style->stroke.set && style->stroke.isColor()) { has_color = true; rgba = style->stroke.value.color.toRGBA32(1.); opacity *= SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); } if (opacity < 1.0) { has_transparency = true; } // get rotation Geom::Affine i2doc = textobj->i2doc_affine(); Geom::Affine wotransl = i2doc.withoutTranslation(); double degrees = -180/M_PI * Geom::atan2(wotransl.xAxis()); bool has_rotation = !Geom::are_near(degrees,0.); // get line-height float line_height; if (style->line_height.unit == SP_CSS_UNIT_NONE) { // unitless 'line-height' (use as-is, computed value is relative value) line_height = style->line_height.computed; } else { // 'line-height' with unit (make relative, computed value is absolute value) line_height = style->line_height.computed / style->font_size.computed; } // write to LaTeX Inkscape::SVGOStringStream os; os.setf(std::ios::fixed); // don't use scientific notation os << " \\put(" << anchor[Geom::X] << "," << anchor[Geom::Y] << "){"; if (has_color) { os << "\\color[rgb]{" << SP_RGBA32_R_F(rgba) << "," << SP_RGBA32_G_F(rgba) << "," << SP_RGBA32_B_F(rgba) << "}"; } if (_pdflatex && has_transparency) { os << "\\transparent{" << opacity << "}"; } if (has_rotation) { os << "\\rotatebox{" << degrees << "}{"; } os << "\\makebox(0,0)" << alignment << "{"; if (line_height != 1) { os << "\\lineheight{" << line_height << "}"; } os << "\\smash{"; os << "\\begin{tabular}[t]" << aligntabular; // Walk through all spans in the text object. // Write span strings to LaTeX, associated with font weight and style. Inkscape::Text::Layout const &layout = *(te_get_layout (textobj)); for (Inkscape::Text::Layout::iterator li = layout.begin(), le = layout.end(); li != le; li.nextStartOfSpan()) { Inkscape::Text::Layout::iterator ln = li; ln.nextStartOfSpan(); Glib::ustring uspanstr = sp_te_get_string_multiline (textobj, li, ln); // escape ampersands uspanstr = Glib::Regex::create("&")->replace_literal(uspanstr, 0, "\\&", (Glib::RegexMatchFlags)0); // escape percent uspanstr = Glib::Regex::create("%")->replace_literal(uspanstr, 0, "\\%", (Glib::RegexMatchFlags)0); const gchar *spanstr = uspanstr.c_str(); if (!spanstr) { continue; } bool is_bold = false, is_italic = false, is_oblique = false; // newline character only -> don't attempt to add style (will break compilation in LaTeX) if (g_strcmp0(spanstr, "\n")) { SPStyle const &spanstyle = *(sp_te_style_at_position (textobj, li)); if (spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_500 || spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_600 || spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_700 || spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_800 || spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_900 || spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_BOLD || spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_BOLDER) { is_bold = true; os << "\\textbf{"; } if (spanstyle.font_style.computed == SP_CSS_FONT_STYLE_ITALIC) { is_italic = true; os << "\\textit{"; } if (spanstyle.font_style.computed == SP_CSS_FONT_STYLE_OBLIQUE) { is_oblique = true; os << "\\textsl{"; // this is an accurate choice if the LaTeX chosen font matches the font in Inkscape. Gives bad results when it is not so... } } // replace carriage return with double slash gchar ** splitstr = g_strsplit(spanstr, "\n", 2); os << splitstr[0]; if (g_strv_length(splitstr) > 1) { os << "\\\\"; } g_strfreev(splitstr); if (is_oblique) { os << "}"; } // oblique end if (is_italic) { os << "}"; } // italic end if (is_bold) { os << "}"; } // bold end } os << "\\end{tabular}"; // tabular end os << "}"; // smash end if (has_rotation) { os << "}"; } // rotatebox end os << "}"; //makebox end os << "}%\n"; // put end fprintf(_stream, "%s", os.str().c_str()); } void LaTeXTextRenderer::sp_flowtext_render(SPFlowtext *flowtext) { /* Flowtext is possible by using a minipage! :) Flowing in rectangle is possible, not in arb shape. */ // Only PDFLaTeX supports importing a single page of a graphics file, // so only PDF backend gets interleaved text/graphics if (_pdflatex && _omittext_state == GRAPHIC_ON_TOP) _omittext_state = NEW_PAGE_ON_GRAPHIC; SPStyle *style = flowtext->style; SPItem *frame_item = flowtext->get_frame(nullptr); SPRect *frame = dynamic_cast(frame_item); if (!frame_item || !frame) { g_warning("LaTeX export: non-rectangular flowed text shapes are not supported, skipping text."); return; // don't know how to handle non-rect frames yet. is quite uncommon for latex users i think } // We will transform the coordinates Geom::Rect framebox = frame->getRect(); // get position and alignment // Align on topleft corner. gchar const *alignment = "[lt]"; gchar const *justification = ""; switch (flowtext->layout.paragraphAlignment(flowtext->layout.begin())) { case Inkscape::Text::Layout::LEFT: justification = "\\raggedright "; break; case Inkscape::Text::Layout::RIGHT: justification = "\\raggedleft "; break; case Inkscape::Text::Layout::CENTER: justification = "\\centering "; case Inkscape::Text::Layout::FULL: default: // no need to add LaTeX code for standard justified output :) break; } // The topleft Corner was calculated after rotating the text which results in a wrong Coordinate. // Now, the topleft Corner is rotated after calculating it Geom::Point pos(framebox.corner(0) * transform()); //topleft corner // determine color and transparency (for now, use rgb color model as it is most native to Inkscape) bool has_color = false; // if the item has no color set, don't force black color bool has_transparency = false; // TODO: how to handle ICC colors? // give priority to fill color guint32 rgba = 0; float opacity = SP_SCALE24_TO_FLOAT(style->opacity.value); if (style->fill.set && style->fill.isColor()) { has_color = true; rgba = style->fill.value.color.toRGBA32(1.); opacity *= SP_SCALE24_TO_FLOAT(style->fill_opacity.value); } else if (style->stroke.set && style->stroke.isColor()) { has_color = true; rgba = style->stroke.value.color.toRGBA32(1.); opacity *= SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); } if (opacity < 1.0) { has_transparency = true; } // get rotation Geom::Affine i2doc = flowtext->i2doc_affine(); Geom::Affine wotransl = i2doc.withoutTranslation(); double degrees = -180/M_PI * Geom::atan2(wotransl.xAxis()); bool has_rotation = !Geom::are_near(degrees,0.); // write to LaTeX Inkscape::SVGOStringStream os; os.setf(std::ios::fixed); // don't use scientific notation os << " \\put(" << pos[Geom::X] << "," << pos[Geom::Y] << "){"; if (has_color) { os << "\\color[rgb]{" << SP_RGBA32_R_F(rgba) << "," << SP_RGBA32_G_F(rgba) << "," << SP_RGBA32_B_F(rgba) << "}"; } if (_pdflatex && has_transparency) { os << "\\transparent{" << opacity << "}"; } if (has_rotation) { os << "\\rotatebox{" << degrees << "}{"; } os << "\\makebox(0,0)" << alignment << "{"; // Scale the x width correctly os << "\\begin{minipage}{" << framebox.width() * transform().expansionX() << "\\unitlength}"; os << justification; // Walk through all spans in the text object. // Write span strings to LaTeX, associated with font weight and style. Inkscape::Text::Layout const &layout = *(te_get_layout(flowtext)); for (Inkscape::Text::Layout::iterator li = layout.begin(), le = layout.end(); li != le; li.nextStartOfSpan()) { SPStyle const &spanstyle = *(sp_te_style_at_position(flowtext, li)); bool is_bold = false, is_italic = false, is_oblique = false; if (spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_500 || spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_600 || spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_700 || spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_800 || spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_900 || spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_BOLD || spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_BOLDER) { is_bold = true; os << "\\textbf{"; } if (spanstyle.font_style.computed == SP_CSS_FONT_STYLE_ITALIC) { is_italic = true; os << "\\textit{"; } if (spanstyle.font_style.computed == SP_CSS_FONT_STYLE_OBLIQUE) { is_oblique = true; os << "\\textsl{"; // this is an accurate choice if the LaTeX chosen font matches the font in Inkscape. Gives bad results when it is not so... } Inkscape::Text::Layout::iterator ln = li; ln.nextStartOfSpan(); Glib::ustring uspanstr = sp_te_get_string_multiline(flowtext, li, ln); const gchar *spanstr = uspanstr.c_str(); if (!spanstr) { continue; } // replace carriage return with double slash gchar ** splitstr = g_strsplit(spanstr, "\n", -1); gchar *spanstr_new = g_strjoinv("\\\\ ", splitstr); os << spanstr_new; g_strfreev(splitstr); g_free(spanstr_new); if (is_oblique) { os << "}"; } // oblique end if (is_italic) { os << "}"; } // italic end if (is_bold) { os << "}"; } // bold end } os << "\\end{minipage}"; if (has_rotation) { os << "}"; // rotatebox end } os << "}"; //makebox end os << "}%\n"; // put end fprintf(_stream, "%s", os.str().c_str()); } void LaTeXTextRenderer::sp_root_render(SPRoot *root) { push_transform(root->c2p); sp_group_render(root); pop_transform(); } void LaTeXTextRenderer::sp_item_invoke_render(SPItem *item) { // Check item's visibility if (item->isHidden()) { return; } SPRoot *root = dynamic_cast(item); if (root) { return sp_root_render(root); } SPGroup *group = dynamic_cast(item); if (group) { return sp_group_render(group); } SPUse *use = dynamic_cast(item); if (use) { return sp_use_render(use); } SPText *text = dynamic_cast(item); if (text) { return sp_text_render(text); } SPFlowtext *flowtext = dynamic_cast(item); if (flowtext) { return sp_flowtext_render(flowtext); } // Only PDFLaTeX supports importing a single page of a graphics file, // so only PDF backend gets interleaved text/graphics if (_pdflatex && (_omittext_state == EMPTY || _omittext_state == NEW_PAGE_ON_GRAPHIC)) { writeGraphicPage(); } _omittext_state = GRAPHIC_ON_TOP; } void LaTeXTextRenderer::renderItem(SPItem *item) { push_transform(item->transform); sp_item_invoke_render(item); pop_transform(); } void LaTeXTextRenderer::writeGraphicPage() { Inkscape::SVGOStringStream os; os.setf(std::ios::fixed); // no scientific notation // strip pathname, as it is probably desired. Having a specific path in the TeX file is not convenient. if (_pdflatex) os << " \\put(0,0){\\includegraphics[width=\\unitlength,page=" << _omittext_page++ << "]{" << _filename << "}}%\n"; else os << " \\put(0,0){\\includegraphics[width=\\unitlength]{" << _filename << "}}%\n"; fprintf(_stream, "%s", os.str().c_str()); } bool LaTeXTextRenderer::setupDocument(SPDocument *doc, bool pageBoundingBox, double bleedmargin_px, SPItem *base) { // The boundingbox calculation here should be exactly the same as the one by CairoRenderer::setupDocument ! if (!base) { base = doc->getRoot(); } Geom::Rect d; if (pageBoundingBox) { d = Geom::Rect::from_xywh(Geom::Point(0,0), doc->getDimensions()); } else { Geom::OptRect bbox = base->documentVisualBounds(); if (!bbox) { g_message("CairoRenderer: empty bounding box."); return false; } d = *bbox; } d.expandBy(bleedmargin_px); // scale all coordinates, such that the width of the image is 1, this is convenient for scaling the image in LaTeX double scale = 1/(d.width()); double _width = d.width() * scale; double _height = d.height() * scale; push_transform(Geom::Translate(-d.corner(3)) * Geom::Scale(scale, -scale)); // write the info to LaTeX Inkscape::SVGOStringStream os; os.setf(std::ios::fixed); // no scientific notation // scaling of the image when including it in LaTeX os << " \\ifx\\svgwidth\\undefined%\n"; os << " \\setlength{\\unitlength}{" << Inkscape::Util::Quantity::convert(d.width(), "px", "pt") << "bp}%\n"; // note: 'bp' is the Postscript pt unit in LaTeX, see LP bug #792384 os << " \\ifx\\svgscale\\undefined%\n"; os << " \\relax%\n"; os << " \\else%\n"; os << " \\setlength{\\unitlength}{\\unitlength * \\real{\\svgscale}}%\n"; os << " \\fi%\n"; os << " \\else%\n"; os << " \\setlength{\\unitlength}{\\svgwidth}%\n"; os << " \\fi%\n"; os << " \\global\\let\\svgwidth\\undefined%\n"; os << " \\global\\let\\svgscale\\undefined%\n"; os << " \\makeatother%\n"; os << " \\begin{picture}(" << _width << "," << _height << ")%\n"; // set \baselineskip equal to fontsize (the closest we can seem to get to CSS "line-height: 1;") // and remove column spacing from tabular os << " \\lineheight{1}%\n"; os << " \\setlength\\tabcolsep{0pt}%\n"; fprintf(_stream, "%s", os.str().c_str()); if (!_pdflatex) writeGraphicPage(); return true; } Geom::Affine const & LaTeXTextRenderer::transform() { return _transform_stack.top(); } void LaTeXTextRenderer::push_transform(Geom::Affine const &tr) { if(!_transform_stack.empty()){ Geom::Affine tr_top = _transform_stack.top(); _transform_stack.push(tr * tr_top); } else { _transform_stack.push(tr); } } void LaTeXTextRenderer::pop_transform() { _transform_stack.pop(); } } /* namespace Internal */ } /* namespace Extension */ } /* namespace Inkscape */ /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :