/** \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 * * Licensed under GNU GPL */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include "latex-text-renderer.h" #include #include #include "libnrtype/Layout-TNG.h" #include <2geom/transforms.h> #include <2geom/rect.h> #include #include "sp-item.h" #include "sp-item-group.h" #include "style.h" #include "sp-root.h" #include "sp-use.h" #include "sp-text.h" #include "sp-flowtext.h" #include "sp-rect.h" #include "text-editing.h" #include "util/units.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, float bleedmargin_px, bool pdflatex) { doc->ensureUpToDate(); SPItem *base = NULL; bool pageBoundingBox = true; if (exportId && strcmp(exportId, "")) { // we want to export the given item only base = dynamic_cast(doc->getObjectById(exportId)); g_assert(base != NULL); pageBoundingBox = exportCanvas; } else { // we want to export the entire document from root base = doc->getRoot(); 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(base); } } delete renderer; return ret; } LaTeXTextRenderer::LaTeXTextRenderer(bool pdflatex) : _stream(NULL), _filename(NULL), _pdflatex(pdflatex), _omittext_state(EMPTY), _omittext_page(1) { push_transform(Geom::identity()); } LaTeXTextRenderer::~LaTeXTextRenderer(void) { 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 succesfull */ bool LaTeXTextRenderer::setTargetFile(gchar const *filename) { if (filename != NULL) { 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 = NULL; 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"; 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(std::vector::const_iterator x = l.begin(); x != l.end(); ++x){ 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) { // 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 (retreived from the anchor point) // Align horizontally on anchorpoint gchar const *alignment = NULL; switch (style->text_anchor.computed) { case SP_CSS_TEXT_ANCHOR_START: alignment = "[lb]"; break; case SP_CSS_TEXT_ANCHOR_END: alignment = "[rb]"; break; case SP_CSS_TEXT_ANCHOR_MIDDLE: default: alignment = "[b]"; break; } Geom::Point anchor = textobj->attributes.firstXY() * transform(); Geom::Point pos(anchor); // 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.); // 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 << "{"; os << "\\smash{"; // smash the text, to be able to put the makebox coordinates at the baseline // 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()) { SPStyle const &spanstyle = *(sp_te_style_at_position (textobj, 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 (textobj, 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 << "}"; // 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(NULL); 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) { sp_root_render(root); } else { SPGroup *group = dynamic_cast(item); if (group) { sp_group_render(group); } else { SPUse *use = dynamic_cast(item); if (use) { sp_use_render(use); } else { SPText *text = dynamic_cast(item); if (text) { sp_text_render(text); } else { SPFlowtext *flowtext = dynamic_cast(item); if (flowtext) { sp_flowtext_render(flowtext); } else { // 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(void) { 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, float 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->desktopVisualBounds(); 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::Scale(scale, scale) ); if (!pageBoundingBox) { push_transform( Geom::Translate( -d.min() ) ); } // flip y-axis push_transform( Geom::Scale(1,-1) * Geom::Translate(0, doc->getHeight().value("px")) ); /// @fixme hardcoded desktop transform! // 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"; 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 :