From 6f986e6acaf4ff68eb0881cc601725ed1fd2ff25 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Sat, 26 Aug 2023 06:13:35 +0900 Subject: [PATCH 01/18] add SVG file --- example/rails/public/testsvg.svg | 328 +++++++ example/rails/public/tux.svg | 1487 ++++++++++++++++++++++++++++++ test/testsvg.svg | 328 +++++++ 3 files changed, 2143 insertions(+) create mode 100644 example/rails/public/testsvg.svg create mode 100644 example/rails/public/tux.svg create mode 100644 test/testsvg.svg diff --git a/example/rails/public/testsvg.svg b/example/rails/public/testsvg.svg new file mode 100644 index 0000000..fe3ece3 --- /dev/null +++ b/example/rails/public/testsvg.svg @@ -0,0 +1,328 @@ + + + + + TCPDF SVG EXAMPLE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + github.com + + + + + + + + SVG + diff --git a/example/rails/public/tux.svg b/example/rails/public/tux.svg new file mode 100644 index 0000000..de8c869 --- /dev/null +++ b/example/rails/public/tux.svg @@ -0,0 +1,1487 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/testsvg.svg b/test/testsvg.svg new file mode 100644 index 0000000..fe3ece3 --- /dev/null +++ b/test/testsvg.svg @@ -0,0 +1,328 @@ + + + + + TCPDF SVG EXAMPLE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + github.com + + + + + + + + SVG + From 386e78a333560d558377926d0fc96b9718f227e3 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Mon, 20 Mar 2023 21:18:22 +0900 Subject: [PATCH 02/18] add SVG Support Method ImageSVG() was added to embedd SVG images (see example n. 6, 39, 58). Note that not all SVG images are supported. - Filter attribute are not supported. See: https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Filter_effects --- README.md | 1 + .../app/controllers/example006_controller.rb | 2 +- .../app/controllers/example039_controller.rb | 2 +- .../app/controllers/example058_controller.rb | 80 + .../rails/app/controllers/example_common.rb | 3 +- lib/rbpdf.rb | 1436 ++++++++++++++++- rbpdf.gemspec | 1 + test/rbpdf_examples_test.rb | 1 + 8 files changed, 1520 insertions(+), 6 deletions(-) create mode 100644 example/rails/app/controllers/example058_controller.rb diff --git a/README.md b/README.md index 099fac6..bfcad39 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ A template plugin allowing the inclusion of ERB-enabled RBPDF template files. * Image - 8bit PNG image support without MiniMagick/RMagick library. - PNG(with alpha channel)/JPEG/GIF/WebP image support. (use MiniMagick or RMagick library) + - SVG image support. (use REXML for XML parsing.) ## diff --git a/example/rails/app/controllers/example006_controller.rb b/example/rails/app/controllers/example006_controller.rb index 8ece7e5..4cf9bee 100644 --- a/example/rails/app/controllers/example006_controller.rb +++ b/example/rails/app/controllers/example006_controller.rb @@ -95,7 +95,7 @@ def index
White cold drink
IMAGES
- test alt attributetest alt attributetest alt attribute + test alt attributetest alt attributetest alt attribute
' # output the HTML content diff --git a/example/rails/app/controllers/example039_controller.rb b/example/rails/app/controllers/example039_controller.rb index e574226..25113b6 100644 --- a/example/rails/app/controllers/example039_controller.rb +++ b/example/rails/app/controllers/example039_controller.rb @@ -59,7 +59,7 @@ def index pdf.write(0, 'Example of HTML Justification', '', 0, 'L', true, 0, false, false, 0) # create some HTML content - html = 'a abc abcdefghijkl abcdef abcdefg abcdefghi a abc abcd test alt attribute abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg start a abc before yellow color after a abc abcd abcdef abcdefg abcdefghi a abc abcd end abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi
abcd abcdef abcdefg abcdefghi
abcd abcde abcdef
' + html = 'a abc abcdefghijkl abcdef abcdefg abcdefghi a abc abcd test alt attribute abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg start a abc before yellow color after a abc abcd abcdef abcdefg abcdefghi a abc abcd end abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi a abc abcd abcdef abcdefg abcdefghi
abcd abcdef abcdefg abcdefghi
abcd abcde abcdef
' # set core font pdf.set_font('helvetica', '', 10) diff --git a/example/rails/app/controllers/example058_controller.rb b/example/rails/app/controllers/example058_controller.rb new file mode 100644 index 0000000..7baa3f3 --- /dev/null +++ b/example/rails/app/controllers/example058_controller.rb @@ -0,0 +1,80 @@ +# coding: UTF-8 +#============================================================+ +# Begin : 2010-04-22 +# Last Update : 2010-05-20 +# +# Description : Example 058 for RBPDF class +# SVG Image +# +# Author: Jun NAITOH +# License: LGPL 2.1 or later +#============================================================+ + +require("example_common.rb") + +class Example058Controller < ApplicationController + def index + # create new PDF document + pdf = RBPDF.new(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false) + + # set document information + pdf.set_creator(PDF_CREATOR) + pdf.set_author(PDF_AUTHOR) + pdf.set_title('RBPDF Example 058') + pdf.set_subject('RBPDF Tutorial') + pdf.set_keywords('RBPDF, PDF, example, test, guide') + + # set default header data + pdf.set_header_data(PDF_HEADER_LOGO, PDF_HEADER_LOGO_WIDTH, PDF_HEADER_TITLE + ' 058', PDF_HEADER_STRING) + + # set header and footer fonts + pdf.set_header_font([PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN]) + pdf.set_footer_font([PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA]) + + # set default monospaced font + pdf.set_default_monospaced_font(PDF_FONT_MONOSPACED) + + # set margins + pdf.set_margins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT) + pdf.set_header_margin(PDF_MARGIN_HEADER) + pdf.set_footer_margin(PDF_MARGIN_FOOTER) + + # set auto page breaks + pdf.set_auto_page_break(true, PDF_MARGIN_BOTTOM) + + # set image scale factor + pdf.set_image_scale(PDF_IMAGE_SCALE_RATIO) + + # set some language-dependent strings + pdf.set_language_array($l) + + # --------------------------------------------------------- + + # set font + pdf.set_font('helvetica', '', 20) + + # add a page + pdf.add_page() + + # NOTE: Uncomment the following line to rasterize SVG image using the ImageMagick library. + # pdf.setRasterizeVectorImages(true); + + pdf.image_svg(PDF_SVG_TEST_TESTSVG, 15, 30, 0, 0, 'https://github.com/naitoh/rbpdf', '', '', 1, false) + + pdf.image_svg(PDF_SVG_TEST_TUX, 30, 100, 0, 100, '', '', '', 0, false) + + pdf.set_font('helvetica', '', 8) + pdf.set_y(195) + txt = '© The copyright holder of the above Tux image is Larry Ewing, allows anyone to use it for any purpose, provided that the copyright holder is properly attributed. Redistribution, derivative work, commercial use, and all other use is permitted.' + pdf.write(0, txt, '', 0, 'L', true, 0, false, false, 0) + + # --------------------------------------------------------- + + # Close and output PDF document + send_data pdf.output(), :type => "application/pdf", :disposition => "inline" + end +end + +#============================================================+ +# END OF FILE +#============================================================+ diff --git a/example/rails/app/controllers/example_common.rb b/example/rails/app/controllers/example_common.rb index ddb130a..6747dbd 100644 --- a/example/rails/app/controllers/example_common.rb +++ b/example/rails/app/controllers/example_common.rb @@ -27,7 +27,6 @@ PDF_IMAGE_DEMO_WEBP="#{PUBLIC}/image_demo.webp" PDF_UTF8TEST_TXT="#{PUBLIC}/utf8test.txt" PDF_TABLE_DATA_DEMO_TXT="#{PUBLIC}/table_data_demo.txt" -PDF_TIGER_AI="#{PUBLIC}/tiger.ai" PDF_PNG_TEST_ALPHA_PNG="#{PUBLIC}/png_test_alpha.png" PDF_PNG_TEST_MSK_ALPHA_PNG="#{PUBLIC}/png_test_msk_alpha.png" PDF_PNG_TEST_NON_ALPHA_PNG="#{PUBLIC}/png_test_non_alpha.png" @@ -35,6 +34,8 @@ PDF_GIF_TEST_NON_ALPHA_PNG="#{PUBLIC}/gif_test_non_alpha.png" PDF_GIF_TEST_ALPHA_GIF="#{PUBLIC}/gif_test_alpha.gif" PDF_WEBP_TEST_ALPHA_PNG="#{PUBLIC}/webp_test_alpha.webp" +PDF_SVG_TEST_TESTSVG="#{PUBLIC}/testsvg.svg" +PDF_SVG_TEST_TUX="#{PUBLIC}/tux.svg" if !defined? send_data def send_data(data, options = {}) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index 061d079..13b4794 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -428,6 +428,84 @@ def initialize(orientation = 'P', unit = 'mm', format = 'A4', unicode = true, e @pdfunit ||= 'mm' @tocpage ||= false + @rasterize_vector_images = false + @svgdir = '' + @svgunit = 'px' + @svggradients = {} + @svggradientid = 0 + @svgdefsmode = false + @svgdefs = {} + @svgclipmode = false + @svgclippaths = {} + @svgclipid = 0 + @svgtext = '' + @svginheritprop = ['clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cursor', 'direction', 'fill', 'fill-opacity', 'fill-rule', 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 'image-rendering', 'kerning', 'letter-spacing', 'marker', 'marker-end', 'marker-mid', 'marker-start', 'pointer-events', 'shape-rendering', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-anchor', 'text-rendering', 'visibility', 'word-spacing', 'writing-mode'] + @svgstyles = [{ + 'alignment-baseline' => 'auto', + 'baseline-shift' => 'baseline', + 'clip' => 'auto', + 'clip-path' => 'none', + 'clip-rule' => 'nonzero', + 'color' => 'black', + 'color-interpolation' => 'sRGB', + 'color-interpolation-filters' => 'linearRGB', + 'color-profile' => 'auto', + 'color-rendering' => 'auto', + 'cursor' => 'auto', + 'direction' => 'ltr', + 'display' => 'inline', + 'dominant-baseline' => 'auto', + 'enable-background' => 'accumulate', + 'fill' => 'black', + 'fill-opacity' => 1, + 'fill-rule' => 'nonzero', + 'filter' => 'none', + 'flood-color' => 'black', + 'flood-opacity' => 1, + 'font' => '', + 'font-family' => 'helvetica', + 'font-size' => 'medium', + 'font-size-adjust' => 'none', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-variant' => 'normal', + 'font-weight' => 'normal', + 'glyph-orientation-horizontal' => '0deg', + 'glyph-orientation-vertical' => 'auto', + 'image-rendering' => 'auto', + 'kerning' => 'auto', + 'letter-spacing' => 'normal', + 'lighting-color' => 'white', + 'marker' => '', + 'marker-end' => 'none', + 'marker-mid' => 'none', + 'marker-start' => 'none', + 'mask' => 'none', + 'opacity' => 1, + 'overflow' => 'auto', + 'pointer-events' => 'visiblePainted', + 'shape-rendering' => 'auto', + 'stop-color' => 'black', + 'stop-opacity' => 1, + 'stroke' => 'none', + 'stroke-dasharray' => 'none', + 'stroke-dashoffset' => 0, + 'stroke-linecap' => 'butt', + 'stroke-linejoin' => 'miter', + 'stroke-miterlimit' => 4, + 'stroke-opacity' => 1, + 'stroke-width' => 1, + 'text-anchor' => 'start', + 'text-decoration' => 'none', + 'text-rendering' => 'auto', + 'unicode-bidi' => 'normal', + 'visibility' => 'visible', + 'word-spacing' => 'normal', + 'writing-mode' => 'lr-tb', + 'text-color' => 'black', + 'transfmatrix' => [1, 0, 0, 1, 0, 0] + }] + ####################### #Some checks @@ -14740,7 +14818,6 @@ def openHTMLTagHandler(dom, key, cell) tag['attribute']['src'].gsub!(/%([0-9a-fA-F]{2})/){$1.hex.chr} img_name = tag['attribute']['src'] - type = getImageFileType(tag['attribute']['src']) tag['attribute']['src'] = get_image_filename(tag['attribute']['src']) if tag['width'].nil? tag['width'] = 0 @@ -14815,7 +14892,12 @@ def openHTMLTagHandler(dom, key, cell) result_img = proc_image_file(tag['attribute']['src']) do |img_file| - Image(img_file, xpos, @y, iw, ih, '', imglink, align, false, 300, '', false, false, border, false, false, true) + type = getImageFileType(tag['attribute']['src']) + if type == 'svg' + image_svg(img_file, xpos, @y, iw, ih, imglink, align, '', border, true) + else + image(img_file, xpos, @y, iw, ih, '', imglink, align, false, 300, '', false, false, border, false, false, true) + end end if result_img or ih != 0 @@ -16958,8 +17040,1356 @@ def getPathPaintOperator(style, default='S') protected :getPathPaintOperator # -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.- - # SVG METHODS (not implement, yet.) + # SVG METHODS # -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.- + require 'rexml/parsers/pullparser' + # + # Embedd a Scalable Vector Graphics (SVG) image. + # NOTE: SVG standard is not yet fully implemented, use the setRasterizeVectorImages() method to enable/disable rasterization of SVG images using ImageMagick library. + # [@param string :file] Name of the SVG file. + # [@param float :x] Abscissa of the upper-left corner. + # [@param float :y] Ordinate of the upper-left corner. + # [@param float :w] Width of the image in the page. If not specified or equal to zero, it is automatically calculated. + # [@param float :h] Height of the image in the page. If not specified or equal to zero, it is automatically calculated. + # [@param mixed :link] URL or identifier returned by AddLink(). + # [@param string :align] Indicates the alignment of the pointer next to image insertion relative to image height. The value can be: + # [@param string :palign] Allows to center or align the image on the current line. Possible values are: + # [@param mixed :border] Indicates if borders must be drawn around the image. The value can be either a number:or a string containing some or all of the following characters (in any order): + # [@param boolean :fitonpage] if true the image is resized to not exceed page dimensions. + # [@since 5.0.000 (2010-05-02)] + # [@access public] + # + def image_svg(file, x='', y='', w=0, h=0, link='', align='', palign='', border=0, fitonpage=false) + error("SVG file not found: #{file}") unless File.exist?(file) + + if @rasterize_vector_images + # convert SVG to raster image using GD or ImageMagick libraries + return Image(file, x, y, w, h, 'SVG', link, align, true, 300, palign, false, false, border, false, false, false) + end + + @svgdir = File.dirname(file) + svgdata = '' + open(file,'rb') do |f| + svgdata = f.read() + end + x = @x if x == '' + y = @y if y == '' + k = @k + ox = 0 + oy = 0 + ow = w + oh = h + aspect_ratio_align = 'xMidYMid' + aspect_ratio_ms = 'meet' + # get original image width and height + regs = svgdata.scan(/]*)>/mi) + if !regs[0][0].nil? && !regs[0][0].empty? + tmp = regs[0][0].scan(/[\s]+x[\s]*=[\s]*"([^"]*)"/mi) + if tmp[0] && !tmp[0].empty? + ox = getHTMLUnitToUnits(tmp[0][0], 0, @svgunit, false) + end + tmp = regs[0][0].scan(/[\s]+y[\s]*=[\s]*"([^"]*)"/mi) + if tmp[0] && !tmp[0].empty? + oy = getHTMLUnitToUnits(tmp[0][0], 0, @svgunit, false) + end + tmp = regs[0][0].scan(/[\s]+width[\s]*=[\s]*"([^"]*)"/mi) + if tmp[0] && !tmp[0].empty? + ow = getHTMLUnitToUnits(tmp[0][0], 1, @svgunit, false) + end + tmp = regs[0][0].scan(/[\s]+height[\s]*=[\s]*"([^"]*)"/mi) + if tmp[0] && !tmp[0].empty? + oh = getHTMLUnitToUnits(tmp[0][0], 1, @svgunit, false) + end + view_box = [] + tmp = regs[0][0].scan(/[\s]+viewBox[\s]*=[\s]*"[\s]*([0-9\.\-]+)[\s]+([0-9\.\-]+)[\s]+([0-9\.]+)[\s]+([0-9\.]+)[\s]*"/mi) + if tmp[0] && !tmp[0].empty? + if tmp[0].size == 4 + tmp[0].each_with_index {|val, key| + view_box[key] = getHTMLUnitToUnits(val, 0, @svgunit, false) + } + ox = view_box[0] + oy = view_box[1] + end + # get aspect ratio + tmp = regs[0][0].scan(/[\s]+preserveAspectRatio[\s]*=[\s]*"([^"]*)"/mi) + if tmp[0] && !tmp[0].empty? + aspect_ratio = tmp[0][0].split(/[\s]+/mi) + case aspect_ratio.size + when 3 + aspect_ratio_align = aspect_ratio[1] + aspect_ratio_ms = aspect_ratio[2] + when 2 + aspect_ratio_align = aspect_ratio[0] + aspect_ratio_ms = aspect_ratio[1] + when 1 + aspect_ratio_align = aspect_ratio[0] + aspect_ratio_ms = 'meet' + end + end + end + end + # calculate image width and height on document + if (w <= 0) && (h <= 0) + # convert image size to document unit + w = ow + h = oh + elsif w <= 0 + w = h * ow / oh + elsif h <= 0 + h = w * oh / ow + end + # Check whether we need a new page first as this does not fit + prev_x = @x + if checkPageBreak(h, y) + y = @y + if @rtl + x += (prev_x - @x) + else + x += (@x - prev_x) + end + end + # resize image to be contained on a single page + if fitonpage + ratio_wh = w / h + if (y + h) > @page_break_trigger + h = @page_break_trigger - y + w = h * ratio_wh + end + if !@rtl && ((x + w) > (@w - @r_margin)) + w = @w - @r_margin - x + h = w / ratio_wh + elsif @rtl && ((x - w) < (@l_margin)) + w = x - @l_margin + h = w / ratio_wh + end + end + # set alignment + @img_rb_y = y + h + # set alignment + if @rtl + case palign + when 'L' + ximg = @l_margin + when 'C' + ximg = (@w + @l_margin - @r_margin - w) / 2 + when 'R' + ximg = @w - @r_margin - w + else + ximg = x - w + end + @img_rb_x = ximg + else + case palign + when 'L' + ximg = @l_margin + when 'C' + ximg = (@w + @l_margin - @r_margin - w) / 2 + when 'R' + ximg = @w - @r_margin - w + else + ximg = x + end + @img_rb_x = ximg + w + end + # store current graphic vars + gvars = getGraphicVars() + # store SVG position and scale factors + svgoffset_x = (ximg - ox) * @k + svgoffset_y = -(y - oy) * @k + if (view_box[2]&.> 0) && (view_box[3]&.> 0) + ow = view_box[2] + oh = view_box[3] + end + svgscale_x = w / ow + svgscale_y = h / oh + # scaling and alignment + if aspect_ratio_align != 'none' + # store current scaling values + svgscale_old_x = svgscale_x + svgscale_old_y = svgscale_y + # force uniform scaling + if aspect_ratio_ms == 'slice' + # the entire viewport is covered by the viewBox + if svgscale_x > svgscale_y + svgscale_y = svgscale_x + elsif svgscale_x < svgscale_y + svgscale_x = svgscale_y + end + else # meet + # the entire viewBox is visible within the viewport + if svgscale_x < svgscale_y + svgscale_y = svgscale_x + elsif svgscale_x > svgscale_y + svgscale_x = svgscale_y + end + end + # correct X alignment + case aspect_ratio_align[1, 3] + when 'Min' + # do nothing + when 'Max' + svgoffset_x += ((w * @k) - (ow * @k * svgscale_x)) + else # 'Mid' + svgoffset_x += (((w * @k) - (ow * @k * svgscale_x)) / 2) + end + # correct Y alignment + case aspect_ratio_align[5..-1] + when 'Min' + # do nothing + when 'Max' + svgoffset_y -= ((h * @k) - (oh * @k * svgscale_y)) + else # 'Mid' + svgoffset_y -= (((h * @k) - (oh * @k * svgscale_y)) / 2) + end + end + # save the current graphic state + out("q#{@epsmarker}") + # set initial clipping mask + rect(x, y, w, h, 'CNZ', {}, {}) + # scale and translate + e = ox * @k * (1 - svgscale_x) + f = (@h - oy) * @k * (1 - svgscale_y) + out(sprintf('%.3f %.3f %.3f %.3f %.3f %.3f cm', svgscale_x, 0, 0, svgscale_y, e + svgoffset_x, f + svgoffset_y)) + + open(file,'rb') do |f| + parser = REXML::Parsers::PullParser.new(f) + while parser.has_next? + res = parser.pull + case res.event_type + when :start_element # sets the element handler method + name = res[0] + attribs = res[1] + startSVGElementHandler(name, attribs) + when :end_element + name = res[0] + endSVGElementHandler(name) + when :cdata # sets the character data handler method + data = res[0] + segSVGContentHandler(data) + when :text # sets the character data handler method + data = res[1] + segSVGContentHandler(data) + end + end + end + + # start parsing an XML document + #if !xml_parse(@parser, svgdata) + # error_message = sprintf("SVG Error: %s at line %d", xml_error_string(xml_get_error_code(@parser)), xml_get_current_line_number(@parser)) + # error(error_message) + #end + + # restore previous graphic state + out("#{@epsmarker}Q") + # restore graphic vars + setGraphicVars(gvars) + unless border == 0 + bx = x + by = y + @x = ximg + if @rtl + @x += w + end + @y = y + cell(w, h, '', border, 0, '', 0, '', 0) + @x = bx + @y = by + end + if link + link(ximg, y, w, h, link, 0) + end + # set pointer to align the successive text/objects + case align + when 'T' + @y = y + @x = @img_rb_x + when 'M' + @y = y + round(h/2) + @x = @img_rb_x + when 'B' + @y = @img_rb_y + @x = @img_rb_x + when 'N' + set_y(@img_rb_y) + end + @endlinex = @img_rb_x + end + +protected + + # + # Get the tranformation matrix from SVG transform attribute + # [@param string :transformation] + # [@return array of transformations] + # [@since 5.0.000 (2010-05-02)] + # [@access protected] + # + def getSVGTransformMatrix(attribute) + # identity matrix + tm = [1, 0, 0, 1, 0, 0] + continue = true + while continue + continue = false + # matrix + if attribute =~ /matrix\(([a-z0-9\-\.]+)[\,\s]+([a-z0-9\-\.]+)[\,\s]+([a-z0-9\-\.]+)[\,\s]+([a-z0-9\-\.]+)[\,\s]+([a-z0-9\-\.]+)[\,\s]+([a-z0-9\-\.]+)\)/mi + a = Regexp.last_match[1].to_f + b = Regexp.last_match[2].to_f + c = Regexp.last_match[3].to_f + d = Regexp.last_match[4].to_f + e = Regexp.last_match[5].to_f + f = Regexp.last_match[6].to_f + attribute = attribute.gsub(Regexp.last_match[0], '') + continue = true + tm = getTransformationMatrixProduct(tm, [a, b, c, d, e, f]) + end + # translate x + if attribute =~ /translate\(([-a-z0-9.]+)\)/mi + attribute = attribute.gsub(Regexp.last_match[0], "translate(#{Regexp.last_match[1]},0)") + continue = true + end + # translate x,y + if attribute =~ /translate\(([-a-z0-9.]+)[,\s]+([-a-z0-9.]+)\)/mi + e = Regexp.last_match[1].to_f + f = Regexp.last_match[2].to_f + attribute = attribute.gsub(Regexp.last_match[0], '') + continue = true + tm = getTransformationMatrixProduct(tm, [1, 0, 0, 1, e, f]) + end + # scale x + if attribute =~ /scale\(([-a-z0-9.]+)\)/mi + attribute = attribute.gsub(Regexp.last_match[0], "scale(#{Regexp.last_match[1]},#{Regexp.last_match[1]})") + continue = true + end + # scale x,y + if attribute =~ /scale\(([-a-z0-9.]+)[,\s]+([-a-z0-9.]+)\)/mi + a = Regexp.last_match[1].to_f + if Regexp.last_match[2] && (Regexp.last_match[2].strip.size > 0) + d = Regexp.last_match[2].to_f + else + d = a + end + attribute = attribute.gsub(Regexp.last_match[0], '') + continue = true + tm = getTransformationMatrixProduct(tm, [a, 0, 0, d, 0, 0]) + end + # rotate ang + if attribute =~ /rotate\(([-a-z0-9.]+)\)/mi + attribute = attribute.gsub(Regexp.last_match[0], "rotate(#{Regexp.last_match[1]},0,0)") + continue = true + end + # rotate ang,x,y + if attribute =~ /rotate\(([-0-9.]+)[,\s]+([-a-z0-9.]+)[,\s]+([-a-z0-9.]+)\)/mi + ang = Regexp.last_match[1].to_f * ::Math::PI / 180 # deg2rad + a = ::Math::cos(ang) + b = ::Math::sin(ang) + c = -b + d = a + x = Regexp.last_match[2].to_f + y = Regexp.last_match[3].to_f + e = (x * (1 - a)) - (y * c) + f = (y * (1 - d)) - (x * b) + attribute = attribute.gsub(Regexp.last_match[0], '') + continue = true + tm = getTransformationMatrixProduct(tm, [a, b, c, d, e, f]) + end + # skewX + if attribute =~ /skewX\(([-0-9.]+)\)/mi + c = ::Math::tan(Regexp.last_match[1].to_f * ::Math::PI / 180 ) # deg2rad + attribute = attribute.gsub(Regexp.last_match[0], '') + continue = true + tm = getTransformationMatrixProduct(tm, [1, 0, c, 1, 0, 0]) + end + # skewY + if attribute =~ /skewY\(([-0-9.]+)\)/mi + b = ::Math::tan(Regexp.last_match[1].to_f * ::Math::PI / 180 ) # deg2rad + attribute = attribute.gsub(Regexp.last_match[0], '') + continue = true + tm = getTransformationMatrixProduct(tm, [1, b, 0, 1, 0, 0]) + end + end + tm + end + + # + # Get the product of two SVG tranformation matrices + # [@param array :ta] first SVG tranformation matrix + # [@param array :tb] second SVG tranformation matrix + # [@return transformation array] + # [@since 5.0.000 (2010-05-02)] + # [@access protected] + # + def getTransformationMatrixProduct(ta, tb) + tm = [] + tm[0] = (ta[0] * tb[0]) + (ta[2] * tb[1]) + tm[1] = (ta[1] * tb[0]) + (ta[3] * tb[1]) + tm[2] = (ta[0] * tb[2]) + (ta[2] * tb[3]) + tm[3] = (ta[1] * tb[2]) + (ta[3] * tb[3]) + tm[4] = (ta[0] * tb[4]) + (ta[2] * tb[5]) + ta[4] + tm[5] = (ta[1] * tb[4]) + (ta[3] * tb[5]) + ta[5] + tm + end + + # + # Convert SVG transformation matrix to PDF. + # [@param array :tm] original SVG transformation matrix + # [@return array] transformation matrix + # [@access protected] + # [@since 5.0.000 (2010-05-02)] + # + def convertSVGtMatrix(tm) + a = tm[0] + b = -tm[1] + c = -tm[2] + d = tm[3] + e = getHTMLUnitToUnits(tm[4], 1, @svgunit, false) * @k + f = -getHTMLUnitToUnits(tm[5], 1, @svgunit, false) * @k + x = 0 + y = @h * @k + e = (x * (1 - a)) - (y * c) + e + f = (y * (1 - d)) - (x * b) + f + [a, b, c, d, e, f] + end + + # + # Apply SVG graphic transformation matrix. + # [@param array :tm] original SVG transformation matrix + # [@access protected] + # [@since 5.0.000 (2010-05-02)] + # + def svg_transform(tm) + transform(convertSVGtMatrix(tm)) + end + + # + # Apply the requested SVG styles (*** TO BE COMPLETED ***) + # [@param array :svgstyle] array of SVG styles to apply + # [@param array :prevsvgstyle] array of previous SVG style + # [@param int :x] X origin of the bounding box + # [@param int :y] Y origin of the bounding box + # [@param int :w] width of the bounding box + # [@param int :h] height of the bounding box + # [@param string :clip_function] clip function + # [@param array :clip_params] array of parameters for clipping function + # [@return object style] + # [@since 5.0.000 (2010-05-02)] + # [@access protected] + # + def setSVGStyles(svgstyle, prevsvgstyle, x=0, y=0, w=1, h=1, clip_function='', clip_params=[]) + objstyle = '' + return objstyle if svgstyle['opacity'].nil? + + # clip-path + if svgstyle['clip-path'] =~ /url\([\s]*\#([^\)]*)\)/mi + clip_path = @svgclippaths[Regexp.last_match[1]] + clip_path.each{ |cp| startSVGElementHandler(cp['name'], cp['attribs'], true) } + end + #setAlpha(1); # reset alpha + # opacity + setAlpha(svgstyle['opacity']) if svgstyle['opacity'] != 1 + + # color + fill_color = convert_html_color_to_dec_array(svgstyle['color']) + set_fill_color_array(fill_color) + # text color + text_color = convert_html_color_to_dec_array(svgstyle['text-color']) + set_text_color_array(text_color) + # clip + if svgstyle['clip'] =~ /rect\(([a-z0-9\-\.]*)[\s]*([a-z0-9\-\.]*)[\s]*([a-z0-9\-\.]*)[\s]*([a-z0-9\-\.]*)\)/mi + top = Regexp.last_match[1] ? getHTMLUnitToUnits(Regexp.last_match[1], 0, @svgunit, false) : 0 + right = Regexp.last_match[2] ? getHTMLUnitToUnits(Regexp.last_match[2], 0, @svgunit, false) : 0 + bottom = Regexp.last_match[3] ? getHTMLUnitToUnits(Regexp.last_match[3], 0, @svgunit, false) : 0 + left = Regexp.last_match[4] ? getHTMLUnitToUnits(Regexp.last_match[4], 0, @svgunit, false) : 0 + cx = x + left + cy = y + top + cw = w - left - right + ch = h - top - bottom + if svgstyle['clip-rule'] == 'evenodd' + clip_rule = 'CNZ' + else + clip_rule = 'CEO' + end + rect(cx, cy, cw, ch, clip_rule, {}, {}) + end + # fill + if svgstyle['fill'] =~ /url\([\s]*\#([^\)]*)\)/mi + # gradient + gradient = @svggradients[Regexp.last_match[1]] + if gradient['xref'] + # reference to another gradient definition + newgradient = @svggradients[gradient['xref']].dup + newgradient['coords'] = gradient['coords'] + newgradient['mode'] = gradient['mode'] + newgradient['gradientUnits'] = gradient['gradientUnits'] + if gradient['gradientTransform'] + newgradient['gradientTransform'] = gradient['gradientTransform'] + end + gradient = newgradient + end + #save current Graphic State + out('q') + #set clipping area + if !clip_function.empty? && self.respond_to?(clip_function, true) + bbox = self.send(clip_function, *clip_params) + if bbox.is_a?(Array) && (bbox.size == 4) + x, y, w, h = bbox + end + end + if gradient['mode'] == 'measure' + if gradient['gradientTransform'] && !gradient['gradientTransform'].empty? + gtm = gradient['gradientTransform'] + # apply transformation matrix + xa = (gtm[0] * gradient['coords'][0]) + (gtm[2] * gradient['coords'][1]) + gtm[4] + ya = (gtm[1] * gradient['coords'][0]) + (gtm[3] * gradient['coords'][1]) + gtm[5] + xb = (gtm[0] * gradient['coords'][2]) + (gtm[2] * gradient['coords'][3]) + gtm[4] + yb = (gtm[1] * gradient['coords'][2]) + (gtm[3] * gradient['coords'][3]) + gtm[5] + if gradient['coords'][4] + gradient['coords'][4] = ::Math.sqrt(((gtm[0] * gradient['coords'][4]) ** 2) + ((gtm[1] * gradient['coords'][4]) ** 2)) + end + gradient['coords'][0] = xa + gradient['coords'][1] = ya + gradient['coords'][2] = xb + gradient['coords'][3] = yb + end + # convert SVG coordinates to user units + gradient['coords'][0] = getHTMLUnitToUnits(gradient['coords'][0], 0, @svgunit, false) + gradient['coords'][1] = getHTMLUnitToUnits(gradient['coords'][1], 0, @svgunit, false) + gradient['coords'][2] = getHTMLUnitToUnits(gradient['coords'][2], 0, @svgunit, false) + gradient['coords'][3] = getHTMLUnitToUnits(gradient['coords'][3], 0, @svgunit, false) + if gradient['coords'][4] + gradient['coords'][4] = getHTMLUnitToUnits(gradient['coords'][4], 0, @svgunit, false) + end + # shift units + if gradient['gradientUnits'] == 'objectBoundingBox' + # convert to SVG coordinate system + gradient['coords'][0] += x + gradient['coords'][1] += y + gradient['coords'][2] += x + gradient['coords'][3] += y + end + # calculate percentages + gradient['coords'][0] = (gradient['coords'][0] - x) / w + gradient['coords'][1] = (gradient['coords'][1] - y) / h + gradient['coords'][2] = (gradient['coords'][2] - x) / w + gradient['coords'][3] = (gradient['coords'][3] - y) / h + if gradient['coords'][4] + gradient['coords'][4] /= w + end + # fix values + gradient['coords'].each_with_index do |val, key| + if val < 0 + gradient['coords'][key] = 0 + elsif val > 1 + gradient['coords'][key] = 1 + end + end + if (gradient['type'] == 2) && (gradient['coords'][0] == gradient['coords'][2]) && (gradient['coords'][1] == gradient['coords'][3]) + # single color (no shading) + gradient['coords'][0] = 1 + gradient['coords'][1] = 0 + gradient['coords'][2] = 0.999 + gradient['coords'][3] = 0 + end + end + # swap Y coordinates + tmp = gradient['coords'][1] + gradient['coords'][1] = gradient['coords'][3] + gradient['coords'][3] = tmp + # set transformation map for gradient + if (gradient['type'] == 3) && (gradient['mode'] == 'measure') + # gradient is always circular + cy = @h - y - (gradient['coords'][1] * (w + h)) + out(sprintf('%.3f 0 0 %.3f %.3f %.3f cm', w * @k, w * @k, x * @k, cy * @k)) + else + out(sprintf('%.3f 0 0 %.3f %.3f %.3f cm', w * @k, h * @k, x * @k, (@h - (y + h)) * @k)) + end + + if gradient['stops'].size > 1 + gradient(gradient['type'], gradient['coords'], gradient['stops'], [], false) + end + elsif svgstyle['fill'] != 'none' + fill_color = convert_html_color_to_dec_array(svgstyle['fill']) + setAlpha(svgstyle['fill-opacity']) if svgstyle['fill-opacity'] != 1 + + set_fill_color_array(fill_color) + if svgstyle['fill-rule'] == 'evenodd' + objstyle << 'F*' + else + objstyle << 'F' + end + end + # stroke + if svgstyle['stroke'] != 'none' + stroke_style = { + 'color' => convert_html_color_to_dec_array(svgstyle['stroke']), + 'width' => getHTMLUnitToUnits(svgstyle['stroke-width'], 0, @svgunit, false), + 'cap' => svgstyle['stroke-linecap'], + 'join' => svgstyle['stroke-linejoin'] + } + if svgstyle['stroke-dasharray'] && !svgstyle['stroke-dasharray'].empty? && (svgstyle['stroke-dasharray'] != 'none') + stroke_style['dash'] = svgstyle['stroke-dasharray'] + end + set_line_style(stroke_style) + objstyle << 'D' + end + # font + unless svgstyle['font'].empty? + if svgstyle['font'] =~ /font-family[\s]*:[\s]*([^\s;"]*)/mi + font_family = Regexp.last_match[1].strip + else + font_family = svgstyle['font-family'] + end + if svgstyle['font'] =~ /font-size[\s]*:[\s]*([^\s;"]*)/mi + font_size = Regexp.last_match[1].strip + else + font_size = svgstyle['font-size'] + end + if svgstyle['font'] =~ /font-style[\s]*:[\s]*([^\s;"]*)/mi + font_style = Regexp.last_match[1].strip + else + font_style = svgstyle['font-style'] + end + if svgstyle['font'] =~ /font-weight[\s]*:[\s]*([^\s;"]*)/mi + font_weight = Regexp.last_match[1].strip + else + font_weight = svgstyle['font-weight'] + end + else + font_family = svgstyle['font-family'] + font_size = svgstyle['font-size'] + font_style = svgstyle['font-style'] + font_weight = svgstyle['font-weight'] + end + font_size = getHTMLUnitToUnits(font_size, prevsvgstyle['font-size'], @svgunit, false) * @k + case font_style + when 'italic' + font_style = 'I' + when 'oblique' + font_style = 'I' + else # 'normal' + font_style = '' + end + case font_weight + when 'bold', 'bolder' + font_style << 'B' + end + case svgstyle['text-decoration'] + when 'underline' + font_style << 'U' + when 'overline' + font_style << 'O' + when 'line-through' + font_style << 'D' + else # 'none' + end + set_font(font_family, font_style, font_size) + objstyle + end + + # + # Draws an SVG path + # [@param string :d] attribute d of the path SVG element + # [@param string :style] Style of rendering. Possible values are: + #
    + #
  • D or empty string: Draw (default).
  • + #
  • F: Fill.
  • + #
  • F*: Fill using the even-odd rule to determine which regions lie inside the clipping path.
  • + #
  • DF or FD: Draw and fill.
  • + #
  • DF* or FD*: Draw and fill using the even-odd rule to determine which regions lie inside the clipping path.
  • + #
  • CNZ: Clipping mode (using the even-odd rule to determine which regions lie inside the clipping path).
  • + #
  • CEO: Clipping mode (using the nonzero winding number rule to determine which regions lie inside the clipping path).
  • + #
+ # [@return array of container box] measures (x, y, w, h) + # [@since 5.0.000 (2010-05-02)] + # [@access protected] + # + def svg_path(d, style='') + # set fill/stroke style + op = getPathPaintOperator(style, '') + return if op.empty? + + paths = d.scan(/([a-zA-Z])[\s]*([^a-zA-Z\"]*)/mi) + x = 0 + y = 0 + x1 = 0 + y1 = 0 + x2 = 0 + y2 = 0 + xmin = 2147483647 + xmax = 0 + ymin = 2147483647 + ymax = 0 + relcoord = false + # draw curve pieces + paths.each_with_index do |val, key| + # get curve type + cmd = val[0].strip + if cmd.downcase == cmd + # use relative coordinated instead of absolute + relcoord = true + xoffset = x + yoffset = y + else + relcoord = false + xoffset = 0 + yoffset = 0 + end + params = [] + if val[1] + # get curve parameters + rawparams = val[1].strip.split(/[,\s]+/mi) + rawparams.each_with_index do |cp, ck| + params[ck] = getHTMLUnitToUnits(cp, 0, @svgunit, false) + end + end + case cmd.upcase + when 'M' # moveto + params.each_with_index do |cp, ck| + if (ck % 2) == 0 + x = cp + xoffset + else + y = cp + yoffset + if ck == 1 + outPoint(x, y) + else + outLine(x, y) + end + xmin = [xmin, x].min + ymin = [ymin, y].min + xmax = [xmax, x].max + ymax = [ymax, y].max + if relcoord + xoffset = x + yoffset = y + end + end + end + when 'L' # lineto + params.each_with_index do |cp, ck| + if (ck % 2) == 0 + x = cp + xoffset + else + y = cp + yoffset + outLine(x, y) + xmin = [xmin, x].min + ymin = [ymin, y].min + xmax = [xmax, x].max + ymax = [ymax, y].max + if relcoord + xoffset = x + yoffset = y + end + end + end + when 'H' # horizontal lineto + params.each_with_index do |cp, ck| + x = cp + xoffset + outLine(x, y) + xmin = [xmin, x].min + xmax = [xmax, x].max + if relcoord + xoffset = x + end + end + when 'V' # vertical lineto + params.each_with_index do |cp, ck| + y = cp + yoffset + outLine(x, y) + ymin = [ymin, y].min + ymax = [ymax, y].max + if relcoord + yoffset = y + end + end + when 'C' # curveto + params.each_with_index do |cp, ck| + params[ck] = cp + if ((ck + 1) % 6) == 0 + x1 = params[ck - 5] + xoffset + y1 = params[ck - 4] + yoffset + x2 = params[ck - 3] + xoffset + y2 = params[ck - 2] + yoffset + x = params[ck - 1] + xoffset + y = params[ck] + yoffset + outCurve(x1, y1, x2, y2, x, y) + xmin = [xmin, x, x1, x2].min + ymin = [ymin, y, y1, y2].min + xmax = [xmax, x, x1, x2].max + ymax = [ymax, y, y1, y2].max + if relcoord + xoffset = x + yoffset = y + end + end + end + when 'S' # shorthand/smooth curveto + params.each_with_index do |cp, ck| + params[ck] = cp + if ((ck + 1) % 4) == 0 + if (key > 0) && ((paths[key - 1][0].upcase == 'C') || (paths[key - 1][0].upcase == 'S')) + x1 = (2 * x) - x2 + y1 = (2 * y) - y2 + else + x1 = x + y1 = y + end + x2 = params[ck - 3] + xoffset + y2 = params[ck - 2] + yoffset + x = params[ck - 1] + xoffset + y = params[ck] + yoffset + outCurve(x1, y1, x2, y2, x, y) + xmin = [xmin, x, x1, x2].min + ymin = [ymin, y, y1, y2].min + xmax = [xmax, x, x1, x2].max + ymax = [ymax, y, y1, y2].max + if relcoord + xoffset = x + yoffset = y + end + end + end + when 'Q' # quadratic B�zier curveto + params.each_with_index do |cp, ck| + params[ck] = cp + if ((ck + 1) % 4) == 0 + # convert quadratic points to cubic points + x1 = params[ck - 3] + xoffset + y1 = params[ck - 2] + yoffset + xa = (x + (2 * x1)) / 3 + ya = (y + (2 * y1)) / 3 + x = params[ck - 1] + xoffset + y = params[ck] + yoffset + xb = (x + (2 * x1)) / 3 + yb = (y + (2 * y1)) / 3 + outCurve(xa, ya, xb, yb, x, y) + xmin = [xmin, x, xa, xb].min + ymin = [ymin, y, ya, yb].min + xmax = [xmax, x, xa, xb].max + ymax = [ymax, y, ya, yb].max + if relcoord + xoffset = x + yoffset = y + end + end + end + when 'T' # shorthand/smooth quadratic bezier curve + params.each_with_index do |cp, ck| + params[ck] = cp + if (ck % 2) != 0 + if (key > 0) && ((paths[key - 1][0].upcase == 'Q') || (paths[key - 1][0].upcase == 'T')) + x1 = (2 * x) - x1 + y1 = (2 * y) - y1 + else + x1 = x + y1 = y + end + # convert quadratic points to cubic points + xa = (x + (2 * x1)) / 3 + ya = (y + (2 * y1)) / 3 + x = params[ck - 1] + xoffset + y = params[ck] + yoffset + xb = (x + (2 * x1)) / 3 + yb = (y + (2 * y1)) / 3 + outCurve(xa, ya, xb, yb, x, y) + xmin = [xmin, x, x1, x2].min + ymin = [ymin, y, y1, y2].min + xmax = [xmax, x, x1, x2].max + ymax = [ymax, y, y1, y2].max + if relcoord + xoffset = x + yoffset = y + end + end + end + when 'A' # elliptical arc + params.each_with_index do |cp, ck| + params[ck] = cp + if ((ck + 1) % 7) == 0 + x0 = x + y0 = y + rx = params[ck - 6].abs + ry = params[ck - 5].abs + ang = -rawparams[ck - 4].to_f + angle = ang * ::Math::PI / 180 # deg2rad + fa = rawparams[ck - 3].to_i # large-arc-flag + fs = rawparams[ck - 2].to_i # sweep-flag + x = params[ck - 1] + xoffset + y = params[ck] + yoffset + cos_ang = ::Math::cos(angle) + sin_ang = ::Math::sin(angle) + a = (x0 - x) / 2 + b = (y0 - y) / 2 + xa = (a * cos_ang) - (b * sin_ang) + ya = (a * sin_ang) + (b * cos_ang) + rx2 = rx * rx + ry2 = ry * ry + xa2 = xa * xa + ya2 = ya * ya + delta = (xa2 / rx2) + (ya2 / ry2) + if delta > 1 + rx *= ::Math.sqrt(delta) + ry *= ::Math.sqrt(delta) + rx2 = rx * rx + ry2 = ry * ry + end + numerator = (rx2 * ry2) - (rx2 * ya2) - (ry2 * xa2) + if numerator < 0 + root = 0 + else + root = ::Math.sqrt(numerator / ((rx2 * ya2) + (ry2 * xa2))) + end + if fa == fs + root *= -1 + end + cax = root * ((rx * ya) / ry) + cay = -root * ((ry * xa) / rx) + # coordinates of ellipse center + cx = (cax * cos_ang) - (cay * sin_ang) + ((x0 + x) / 2) + cy = (cax * sin_ang) + (cay * cos_ang) + ((y0 + y) / 2) + # get angles + angs = getVectorsAngle(1, 0, ((xa - cax) / rx), ((cay - ya) / ry)) + dang = getVectorsAngle(((xa - cax) / rx), ((ya - cay) / ry), ((-xa - cax) / rx), ((-ya - cay) / ry)) + if (fs == 0) && (dang > 0) + dang -= (2 * ::Math::PI) + elsif (fs == 1) && (dang < 0) + dang += (2 * ::Math::PI) + end + angf = angs - dang + if (fs == 1) && (angs > angf) + tmp = angs + angs = angf + angf = tmp + end + angs = angs * 180 / Math::PI # rad2deg + angf = angf * 180 / Math::PI # rad2deg + pie = false + if (key + 1 < paths.size) && paths[key + 1][0] && (paths[key + 1][0].strip == 'z') + pie = true + end + outellipticalarc(cx, cy, rx, ry, ang, angs, angf, pie, 2) + outPoint(x, y) + xmin = [xmin, x].min + ymin = [ymin, y].min + xmax = [xmax, x].max + ymax = [ymax, y].max + if relcoord + xoffset = x + yoffset = y + end + end + end + when 'Z' + out('h') + end + end # end paths.each_with_index + + out(op) unless op.empty? + + [xmin, ymin, xmax - xmin, ymax - ymin] + end + + # + # Returns the angle in radiants between two vectors + # [@param int :x1] X coordiante of first vector point + # [@param int :y1] Y coordiante of first vector point + # [@param int :x2] X coordiante of second vector point + # [@param int :y2] Y coordiante of second vector point + # [@since 5.0.000 (2010-05-04)] + # [@access protected] + # + def getVectorsAngle(x1, y1, x2, y2) + dprod = (x1 * x2) + (y1 * y2) + dist1 = ::Math.sqrt((x1 * x1) + (y1 * y1)) + dist2 = ::Math.sqrt((x2 * x2) + (y2 * y2)) + angle = ::Math.acos((dprod / (dist1 * dist2)).round(3)) + if angle.nan? + angle = ::Math::PI + end + if ((x1 * y2) - (x2 * y1)) < 0 + angle *= -1 + end + angle + end + + # + # Sets the opening SVG element handler function for the XML parser. (*** TO BE COMPLETED ***) + # [@param string :name] The first parameter, name, contains the name of the element for which this handler is called. If case-folding is in effect for this parser, the element name will be in uppercase letters. + # [@param array :attribs] The second parameter, attribs, contains an associative array with the element's attributes (if any). The keys of this array are the attribute names, the values are the attribute values. Attribute names are case-folded on the same criteria as element names. Attribute values are not case-folded. The original order of the attributes can be retrieved by walking through attribs the normal way, using each(). The first key in the array was the first attribute, and so on. + # [@param bool :clipping] The third parameter, set clipping mode. + # [@since 5.0.000 (2010-05-02)] + # [@access protected] + # + def startSVGElementHandler(name, attribs, clipping = false) + # check if we are in clip mode + if @svgclipmode + @svgclippaths[@svgclipid] << {'name' => name, 'attribs' => attribs} + return + end + if @svgdefsmode && !['clipPath', 'linearGradient', 'radialGradient', 'stop'].include?(name) + @svgdefs[attribs['id']] = {'name' => name, 'attribs' => attribs} + return + end + + # get styling properties + prev_svgstyle = @svgstyles[@svgstyles.size - 1] # previous style + svgstyle = {} # current style + if attribs['style'] + attribs['style'] = ';' + attribs['style'] + end + prev_svgstyle.each do |key, val| + if attribs[key] + if attribs[key] == 'inherit' + svgstyle[key] = val + else + svgstyle[key] = attribs[key] + end + elsif attribs['style'] + # CSS style syntax + if attribs['style'] =~ /[;"\s]#{key}\s*:\s*([^;"\s]*)/mi + if Regexp.last_match[1] == 'inherit' + svgstyle[key] = val + else + svgstyle[key] = Regexp.last_match[1] + end + else + # default value + svgstyle[key] = @svgstyles[0][key] + end + else + if @svginheritprop.include? key + # inherit previous value + svgstyle[key] = val + else + # default value + svgstyle[key] = @svgstyles[0][key] + end + end + end + # transformation matrix + tm = @svgstyles[@svgstyles.size - 1]['transfmatrix'] + if attribs['transform'] && !attribs['transform'].empty? + tm = getTransformationMatrixProduct(tm, getSVGTransformMatrix(attribs['transform'])) + end + svgstyle['transfmatrix'] = tm + # process tag + case name + when 'defs' + @svgdefsmode = true + when 'clipPath' # clipPath + @svgclipmode = true + @svgclipid = attribs['id'] + @svgclippaths[@svgclipid] = [] + when 'svg' # start of SVG object + when 'g' # group together related graphics elements + @svgstyles.push(svgstyle) + start_transform() + setSVGStyles(svgstyle, prev_svgstyle) + when 'linearGradient' + @svggradientid = attribs['id'] + @svggradients[@svggradientid] = {} + @svggradients[@svggradientid]['type'] = 2 + @svggradients[@svggradientid]['stops'] = [] + if attribs['gradientUnits'] + @svggradients[@svggradientid]['gradientUnits'] = attribs['gradientUnits'] + else + @svggradients[@svggradientid]['gradientUnits'] = 'objectBoundingBox' + end + #attribs['spreadMethod'] + x1 = attribs['x1'] ? attribs['x1'].to_f : 0 + y1 = attribs['y1'] ? attribs['y1'].to_f : 0 + x2 = attribs['x2'] ? attribs['x2'].to_f : 1 + y2 = attribs['y2'] ? attribs['y2'].to_f : 0 + if attribs['x1'] && (attribs['x1'][-1] != '%') + @svggradients[@svggradientid]['mode'] = 'measure' + else + @svggradients[@svggradientid]['mode'] = 'percentage' + end + if attribs['gradientTransform'] + @svggradients[@svggradientid]['gradientTransform'] = getSVGTransformMatrix(attribs['gradientTransform']) + end + @svggradients[@svggradientid]['coords'] = [x1, y1, x2, y2] + if attribs['xlink:href'] && !attribs['xlink:href'].empty? + # gradient is defined on another place + @svggradients[@svggradientid]['xref'] = attribs['xlink:href'][1..-1] + end + when 'radialGradient' + @svggradientid = attribs['id'] + @svggradients[@svggradientid] = {} + @svggradients[@svggradientid]['type'] = 3 + @svggradients[@svggradientid]['stops'] = [] + if attribs['gradientUnits'] + @svggradients[@svggradientid]['gradientUnits'] = attribs['gradientUnits'] + else + @svggradients[@svggradientid]['gradientUnits'] = 'objectBoundingBox' + end + #attribs['spreadMethod'] + cx = attribs['cx'] ? attribs['cx'].to_f : 0.5 + cy = attribs['cy'] ? attribs['cy'].to_f : 0.5 + fx = attribs['fx'] ? attribs['fx'].to_f : cx + fy = attribs['fy'] ? attribs['fy'].to_f : cy + r = attribs['r'] ? attribs['r'].to_f : 0.5 + if attribs['cx'] && (attribs['cx'][-1] != '%') + @svggradients[@svggradientid]['mode'] = 'measure' + else + @svggradients[@svggradientid]['mode'] = 'percentage' + end + if attribs['gradientTransform'] + @svggradients[@svggradientid]['gradientTransform'] = getSVGTransformMatrix(attribs['gradientTransform']) + end + @svggradients[@svggradientid]['coords'] = [cx, cy, fx, fy, r] + if attribs['xlink:href'] && !attribs['xlink:href'].empty? + # gradient is defined on another place + @svggradients[@svggradientid]['xref'] = attribs['xlink:href'][1..-1] + end + when 'stop' + # gradient stops + if attribs['offset'][-1] == '%' + offset = attribs['offset'][0...-1].to_f / 100 + else + offset = attribs['offset'].to_f + if offset > 1 + offset /= 100 + end + end + stop_color = svgstyle['stop-color'] ? convert_html_color_to_dec_array(svgstyle['stop-color']) : 'black' + opacity = svgstyle['stop-opacity'] ? svgstyle['stop-opacity'].to_f : 1 + @svggradients[@svggradientid]['stops'] << {'offset' => offset, 'color' => stop_color, 'opacity' => opacity} + when 'path' # paths + d = attribs['d'].strip + if clipping + svg_transform(tm) + svg_path(d, 'CNZ') + else + start_transform() + svg_transform(tm) + obstyle = setSVGStyles(svgstyle, prev_svgstyle, 0, 0, 1, 1, 'svg_path', [d, 'CNZ']) + unless obstyle.empty? + svg_path(d, obstyle) + end + stop_transform() + end + # shapes + when 'rect' + x = attribs['x'] ? getHTMLUnitToUnits(attribs['x'], 0, @svgunit, false) : 0 + y = attribs['y'] ? getHTMLUnitToUnits(attribs['y'], 0, @svgunit, false) : 0 + w = attribs['width'] ? getHTMLUnitToUnits(attribs['width'], 0, @svgunit, false) : 0 + h = attribs['height'] ? getHTMLUnitToUnits(attribs['height'], 0, @svgunit, false) : 0 + rx = attribs['rx'] ? getHTMLUnitToUnits(attribs['rx'], 0, @svgunit, false) : 0 + ry = attribs['ry'] ? getHTMLUnitToUnits(attribs['ry'], 0, @svgunit, false) : rx + # svgstyle['stroke'] + # svgstyle['fill'] + if clipping + svg_transform(tm) + rounded_rect_xy(x, y, w, h, rx, ry, '1111', 'CNZ') + else + start_transform() + svg_transform(tm) + obstyle = setSVGStyles(svgstyle, prev_svgstyle, x, y, w, h, 'rounded_rect_xy', [x, y, w, h, rx, ry, '1111', 'CNZ']) + unless obstyle.empty? + # obstyle = D + rounded_rect_xy(x, y, w, h, rx, ry, '1111', obstyle) + end + stop_transform() + end + when 'circle' + cx = attribs['cx'] ? getHTMLUnitToUnits(attribs['cx'], 0, @svgunit, false) : 0 + cy = attribs['cy'] ? getHTMLUnitToUnits(attribs['cy'], 0, @svgunit, false) : 0 + r = attribs['r'] ? getHTMLUnitToUnits(attribs['r'], 0, @svgunit, false) : 0 + x = cx - r + y = cy - r + w = 2 * r + h = w + if clipping + svg_transform(tm) + circle(cx, cy, r, 0, 360, 'CNZ', nil, nil, 8) + else + start_transform() + svg_transform(tm) + obstyle = setSVGStyles(svgstyle, prev_svgstyle, x, y, w, h, 'circle', [cx, cy, r, 0, 360, 'CNZ']) + unless obstyle.empty? + circle(cx, cy, r, 0, 360, obstyle, nil, nil, 8) + end + stop_transform() + end + when 'ellipse' + cx = attribs['cx'] ? getHTMLUnitToUnits(attribs['cx'], 0, @svgunit, false) : 0 + cy = attribs['cy'] ? getHTMLUnitToUnits(attribs['cy'], 0, @svgunit, false) : 0 + rx = attribs['rx'] ? getHTMLUnitToUnits(attribs['rx'], 0, @svgunit, false) : 0 + ry = attribs['ry'] ? getHTMLUnitToUnits(attribs['ry'], 0, @svgunit, false) : 0 + x = cx - rx + y = cy - ry + w = 2 * rx + h = 2 * ry + if clipping + svg_transform(tm) + ellipse(cx, cy, rx, ry, 0, 0, 360, 'CNZ', nil, nil, 8) + else + start_transform() + svg_transform(tm) + obstyle = setSVGStyles(svgstyle, prev_svgstyle, x, y, w, h, 'ellipse', [cx, cy, rx, ry, 0, 0, 360, 'CNZ']) + unless obstyle.empty? + ellipse(cx, cy, rx, ry, 0, 0, 360, obstyle, nil, nil, 8) + end + stop_transform() + end + when 'line' + x1 = attribs['x1'] ? getHTMLUnitToUnits(attribs['x1'], 0, @svgunit, false) : 0 + y1 = attribs['y1'] ? getHTMLUnitToUnits(attribs['y1'], 0, @svgunit, false) : 0 + x2 = attribs['x2'] ? getHTMLUnitToUnits(attribs['x2'], 0, @svgunit, false) : 0 + y2 = attribs['y2'] ? getHTMLUnitToUnits(attribs['y2'], 0, @svgunit, false) : 0 + x = x1 + y = y1 + w = (x2 - x1).abs + h = (y2 - y1).abs + unless clipping + start_transform() + svg_transform(tm) + obstyle = setSVGStyles(svgstyle, prev_svgstyle, x, y, w, h, 'line', [x1, y1, x2, y2]) + line(x1, y1, x2, y2) + stop_transform() + end + when 'polyline', 'polygon' + points = attribs['points'] ? attribs['points'] : '0 0' + points = points.strip + # note that point may use a complex syntax not covered here + points = points.split(/[\,\s]+/mi) + if points.size >= 4 + p = [] + xmin = 2147483647 + xmax = 0 + ymin = 2147483647 + ymax = 0 + points.each_with_index do |val, key| + p[key] = getHTMLUnitToUnits(val, 0, @svgunit, false) + if (key % 2) == 0 + # X coordinate + xmin = [xmin, p[key]].min + xmax = [xmax, p[key]].max + else + # Y coordinate + ymin = [ymin, p[key]].min + ymax = [ymax, p[key]].max + end + end + x = xmin + y = ymin + w = xmax - xmin + h = ymax - ymin + if name == 'polyline' + start_transform() + svg_transform(tm) + obstyle = setSVGStyles(svgstyle, prev_svgstyle, x, y, w, h, 'poly_line', [p, 'CNZ']) + poly_line(p, 'D') + stop_transform() + else # polygon + if clipping + svg_transform(tm) + polygon(p, 'CNZ', nil, nil, true) + else + start_transform() + svg_transform(tm) + obstyle = setSVGStyles(svgstyle, prev_svgstyle, x, y, w, h, 'polygon', [p, 'CNZ']) + unless obstyle.empty? + polygon(p, obstyle, nil, nil, true) + end + stop_transform() + end + end + end + when 'image' # image + if attribs['xlink:href'] && !attribs['xlink:href'].empty? + x = attribs['x'] ? getHTMLUnitToUnits(attribs['x'], 0, @svgunit, false) : 0 + y = attribs['y'] ? getHTMLUnitToUnits(attribs['y'], 0, @svgunit, false) : 0 + w = attribs['width'] ? getHTMLUnitToUnits(attribs['width'], 0, @svgunit, false) : 0 + h = attribs['height'] ? getHTMLUnitToUnits(attribs['height'], 0, @svgunit, false) : 0 + img = attribs['xlink:href'] + unless clipping + start_transform() + svg_transform(tm) + obstyle = setSVGStyles(svgstyle, prev_svgstyle, x, y, w, h) + # fix image path + if !empty_string(@svgdir) && ((img[0, 1] == '.') || (File.basename(img) == img)) + # replace relative path with full server path + img = @svgdir + '/' + img + end + #if (img[0, 1] == '/') && (_SERVER['DOCUMENT_ROOT'] != '/') + # findroot = strpos(img, _SERVER['DOCUMENT_ROOT']) + # if (findroot === false) || (findroot > 1) + # # replace relative path with full server path + # img = _SERVER['DOCUMENT_ROOT'].img + # end + #end + #img = urldecode(img) + img.gsub!(/%([0-9a-fA-F]{2})/){$1.hex.chr} + + #testscrtype = parse_url(img) + #if testscrtype['query'].nil? || testscrtype['query'].empty? + # convert URL to server path + # img = img.gsub(@@k_path_url, @@k_path_main) + #end + image(img, x, y, w, h) + stop_transform() + end + end + when 'text', 'tspan' # text + # only basic support - advanced features must be implemented + x = attribs['x'] ? getHTMLUnitToUnits(attribs['x'], 0, @svgunit, false) : 0 + y = attribs['y'] ? getHTMLUnitToUnits(attribs['y'], 0, @svgunit, false) : 0 + svgstyle['text-color'] = svgstyle['fill'] + @svgtext = '' + start_transform() + svg_transform(tm) + obstyle = setSVGStyles(svgstyle, prev_svgstyle, x, y, 1, 1) + set_xy(x, y, true) + when 'use' # use + if attribs['xlink:href'] + use = @svgdefs[attribs['xlink:href'][1..-1]] + if attribs['xlink:href'] + attribs.delete('xlink:href') + end + if attribs['id'] + attribs.delete('id') + end + attribs = use['attribs'].merge(attribs) + startSVGElementHandler(use['name'], use['attribs']) + end + end + end + + # + # Sets the closing SVG element handler function for the XML parser. + # [@param string :name] The second parameter, name, contains the name of the element for which this handler is called. If case-folding is in effect for this parser, the element name will be in uppercase letters. + # [@since 5.0.000 (2010-05-02)] + # [@access protected] + # + def endSVGElementHandler(name) + case name + when 'defs' + @svgdefsmode = false + when 'clipPath' # clipPath + @svgclipmode = false + when 'g' + # ungroup: remove last style from array + @svgstyles.pop + stop_transform() + when 'text', 'tspan' + # print text + cell(0, 0, @svgtext.strip, 0, 0, '', 0, '', 0, false, 'L', 'T') + stop_transform() + end + end + + # + # Sets the character data handler function for the XML parser. + # [@param string :data] The second parameter, data, contains the character data as a string. + # [@since 5.0.000 (2010-05-02)] + # [@access protected] + # + def segSVGContentHandler(data) + @svgtext << data + end + + # --- END SVG METHODS ----------------------------- + end # END OF RBPDF CLASS #TODO 2007-05-25 (EJM) Level=0 - diff --git a/rbpdf.gemspec b/rbpdf.gemspec index 45a5f9b..003074f 100644 --- a/rbpdf.gemspec +++ b/rbpdf.gemspec @@ -33,6 +33,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency "htmlentities" spec.add_runtime_dependency "rbpdf-font", "~> 1.19.0" + spec.add_runtime_dependency "rexml" spec.required_ruby_version = '>= 2.3.0' spec.add_development_dependency "bundler" diff --git a/test/rbpdf_examples_test.rb b/test/rbpdf_examples_test.rb index 0777d93..4397337 100644 --- a/test/rbpdf_examples_test.rb +++ b/test/rbpdf_examples_test.rb @@ -60,6 +60,7 @@ class RbpdfTest < Test::Unit::TestCase '054 : XHTML Form' => '054', '055 : Display all characters available on core fonts.' => '055', '057 : Cell vertical alignments' => '057', + '058 : SVG Image' => '058', '059 : Table Of Content using HTML templates.' => '059', '060 : Advanced page settings.' => '060', '061 : XHTML + CSS' => '061', From ab511aa94aee7ec1adcdd7761082c40f5067d860 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Sun, 19 Nov 2023 22:00:19 +0900 Subject: [PATCH 03/18] fix setSVGStyles() 'font-family' attribute --- lib/rbpdf.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index 13b4794..fbc0e82 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -17681,7 +17681,18 @@ def setSVGStyles(svgstyle, prevsvgstyle, x=0, y=0, w=1, h=1, clip_function='', c font_style << 'D' else # 'none' end - set_font(font_family, font_style, font_size) + + fontname = nil + fontslist = font_family.downcase.split(',') + fontslist.each {|font| + font = font.downcase.strip + if @fontlist.include?(font) or @fontkeys.include?(font) + fontname = font + break + end + } + + set_font(fontname || 'helvetica', font_style, font_size) objstyle end From c41d5d809f0b6eafaaa0c9a35d200659334f7247 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Sat, 26 Aug 2023 18:09:21 +0900 Subject: [PATCH 04/18] ImageSVG : Fixes handling of svg tag attributes --- lib/rbpdf.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index fbc0e82..37fda8f 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -17084,24 +17084,24 @@ def image_svg(file, x='', y='', w=0, h=0, link='', align='', palign='', border=0 # get original image width and height regs = svgdata.scan(/]*)>/mi) if !regs[0][0].nil? && !regs[0][0].empty? - tmp = regs[0][0].scan(/[\s]+x[\s]*=[\s]*"([^"]*)"/mi) + tmp = regs[0][0].scan(/\s+x\s*=\s*["']([^"']*)["']/mi) if tmp[0] && !tmp[0].empty? ox = getHTMLUnitToUnits(tmp[0][0], 0, @svgunit, false) end - tmp = regs[0][0].scan(/[\s]+y[\s]*=[\s]*"([^"]*)"/mi) + tmp = regs[0][0].scan(/\s+y\s*=\s*["']([^"']*)["']/mi) if tmp[0] && !tmp[0].empty? oy = getHTMLUnitToUnits(tmp[0][0], 0, @svgunit, false) end - tmp = regs[0][0].scan(/[\s]+width[\s]*=[\s]*"([^"]*)"/mi) + tmp = regs[0][0].scan(/\s+width\s*=\s*["']([^"']*)["']/mi) if tmp[0] && !tmp[0].empty? ow = getHTMLUnitToUnits(tmp[0][0], 1, @svgunit, false) end - tmp = regs[0][0].scan(/[\s]+height[\s]*=[\s]*"([^"]*)"/mi) + tmp = regs[0][0].scan(/\s+height\s*=\s*["']([^"']*)["']/mi) if tmp[0] && !tmp[0].empty? oh = getHTMLUnitToUnits(tmp[0][0], 1, @svgunit, false) end view_box = [] - tmp = regs[0][0].scan(/[\s]+viewBox[\s]*=[\s]*"[\s]*([0-9\.\-]+)[\s]+([0-9\.\-]+)[\s]+([0-9\.]+)[\s]+([0-9\.]+)[\s]*"/mi) + tmp = regs[0][0].scan(/\s+viewBox\s*=\s*["']\s*([-0-9.]+)\s+([-0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\s*["']/mi) if tmp[0] && !tmp[0].empty? if tmp[0].size == 4 tmp[0].each_with_index {|val, key| @@ -17128,6 +17128,10 @@ def image_svg(file, x='', y='', w=0, h=0, link='', align='', palign='', border=0 end end end + if (view_box[2]&.> 0) && (view_box[3]&.> 0) + ow = view_box[2] + oh = view_box[3] + end # calculate image width and height on document if (w <= 0) && (h <= 0) # convert image size to document unit @@ -17196,10 +17200,6 @@ def image_svg(file, x='', y='', w=0, h=0, link='', align='', palign='', border=0 # store SVG position and scale factors svgoffset_x = (ximg - ox) * @k svgoffset_y = -(y - oy) * @k - if (view_box[2]&.> 0) && (view_box[3]&.> 0) - ow = view_box[2] - oh = view_box[3] - end svgscale_x = w / ow svgscale_y = h / oh # scaling and alignment From 1ec7b66fb08aad6d119fd3f5bb288b387c019566 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Sat, 2 Sep 2023 18:07:09 +0900 Subject: [PATCH 05/18] Refactoring of ImageSVG method Separated svg tag parsing into parse_svg_tag_attributes method. Changed to use rexml pullparser instead of regular expressions. --- lib/rbpdf.rb | 130 ++++++++++++++++-------------- test/rbpdf_image_svg_func_test.rb | 52 ++++++++++++ 2 files changed, 121 insertions(+), 61 deletions(-) create mode 100644 test/rbpdf_image_svg_func_test.rb diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index 37fda8f..cd62c78 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -17068,70 +17068,12 @@ def image_svg(file, x='', y='', w=0, h=0, link='', align='', palign='', border=0 end @svgdir = File.dirname(file) - svgdata = '' - open(file,'rb') do |f| - svgdata = f.read() - end x = @x if x == '' y = @y if y == '' k = @k - ox = 0 - oy = 0 - ow = w - oh = h - aspect_ratio_align = 'xMidYMid' - aspect_ratio_ms = 'meet' - # get original image width and height - regs = svgdata.scan(/]*)>/mi) - if !regs[0][0].nil? && !regs[0][0].empty? - tmp = regs[0][0].scan(/\s+x\s*=\s*["']([^"']*)["']/mi) - if tmp[0] && !tmp[0].empty? - ox = getHTMLUnitToUnits(tmp[0][0], 0, @svgunit, false) - end - tmp = regs[0][0].scan(/\s+y\s*=\s*["']([^"']*)["']/mi) - if tmp[0] && !tmp[0].empty? - oy = getHTMLUnitToUnits(tmp[0][0], 0, @svgunit, false) - end - tmp = regs[0][0].scan(/\s+width\s*=\s*["']([^"']*)["']/mi) - if tmp[0] && !tmp[0].empty? - ow = getHTMLUnitToUnits(tmp[0][0], 1, @svgunit, false) - end - tmp = regs[0][0].scan(/\s+height\s*=\s*["']([^"']*)["']/mi) - if tmp[0] && !tmp[0].empty? - oh = getHTMLUnitToUnits(tmp[0][0], 1, @svgunit, false) - end - view_box = [] - tmp = regs[0][0].scan(/\s+viewBox\s*=\s*["']\s*([-0-9.]+)\s+([-0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\s*["']/mi) - if tmp[0] && !tmp[0].empty? - if tmp[0].size == 4 - tmp[0].each_with_index {|val, key| - view_box[key] = getHTMLUnitToUnits(val, 0, @svgunit, false) - } - ox = view_box[0] - oy = view_box[1] - end - # get aspect ratio - tmp = regs[0][0].scan(/[\s]+preserveAspectRatio[\s]*=[\s]*"([^"]*)"/mi) - if tmp[0] && !tmp[0].empty? - aspect_ratio = tmp[0][0].split(/[\s]+/mi) - case aspect_ratio.size - when 3 - aspect_ratio_align = aspect_ratio[1] - aspect_ratio_ms = aspect_ratio[2] - when 2 - aspect_ratio_align = aspect_ratio[0] - aspect_ratio_ms = aspect_ratio[1] - when 1 - aspect_ratio_align = aspect_ratio[0] - aspect_ratio_ms = 'meet' - end - end - end - end - if (view_box[2]&.> 0) && (view_box[3]&.> 0) - ow = view_box[2] - oh = view_box[3] - end + + ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms = parse_svg_tag_attributes(file, w, h) + # calculate image width and height on document if (w <= 0) && (h <= 0) # convert image size to document unit @@ -17315,6 +17257,72 @@ def image_svg(file, x='', y='', w=0, h=0, link='', align='', palign='', border=0 @endlinex = @img_rb_x end + def parse_svg_tag_attributes(file, w, h) + ox = 0 + oy = 0 + ow = w + oh = h + aspect_ratio_align = 'xMidYMid' + aspect_ratio_ms = 'meet' + open(file,'rb') do |f| + parser = REXML::Parsers::PullParser.new(f) + + while parser.has_next? + res = parser.pull + if (res.event_type == :start_element) && (res[0] == 'svg') + attribs = res[1] + + if attribs['x'] && !attribs['x'].empty? + ox = getHTMLUnitToUnits(attribs['x'], 0, @svgunit, false) + end + if attribs['y'] && !attribs['y'].empty? + oy = getHTMLUnitToUnits(attribs['y'], 0, @svgunit, false) + end + if attribs['width'] && !attribs['width'].empty? + ow = getHTMLUnitToUnits(attribs['width'], 1, @svgunit, false) + end + if attribs['height'] && !attribs['height'].empty? + oh = getHTMLUnitToUnits(attribs['height'], 1, @svgunit, false) + end + if attribs['viewBox'] && !attribs['viewBox'].empty? + tmp = attribs['viewBox'].strip.split(/[,\s]+/) + view_box = [] + if tmp.size == 4 + tmp.each_with_index {|val, key| + view_box[key] = getHTMLUnitToUnits(val, 0, @svgunit, false) + } + ox = view_box[0] + oy = view_box[1] + end + if (view_box[2]&.> 0) && (view_box[3]&.> 0) + ow = view_box[2] + oh = view_box[3] + end + # get aspect ratio + if attribs['preserveAspectRatio'] && attribs['preserveAspectRatio'].empty? + aspect_ratio = attribs['preserveAspectRatio'].split + case aspect_ratio.size + when 3 + aspect_ratio_align = aspect_ratio[1] + aspect_ratio_ms = aspect_ratio[2] + when 2 + aspect_ratio_align = aspect_ratio[0] + aspect_ratio_ms = aspect_ratio[1] + when 1 + aspect_ratio_align = aspect_ratio[0] + aspect_ratio_ms = 'meet' + end + end + end + break + end + end + end + + [ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms] + end + private :parse_svg_tag_attributes + protected # diff --git a/test/rbpdf_image_svg_func_test.rb b/test/rbpdf_image_svg_func_test.rb new file mode 100644 index 0000000..2f454f3 --- /dev/null +++ b/test/rbpdf_image_svg_func_test.rb @@ -0,0 +1,52 @@ +# coding: ASCII-8BIT +# Copyright (c) 2011-2023 NAITOH Jun +# Released under the MIT license +# http://www.opensource.org/licenses/MIT + +require 'test_helper' + +class RbpdfTest < Test::Unit::TestCase + class MYPDF < RBPDF + def parse_svg_tag_attributes(file, w, h) + super + end + end + + test "SVG parse_svg_tag_attributes x y width height test" do + tf = Tempfile.open(['test', '.svg']) do |fp| + fp.puts '' + fp + end + + pdf = MYPDF.new + ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms = pdf.parse_svg_tag_attributes(tf.path, 0, 0) + assert_equal 3.5277777777777772, ox + assert_equal 7.0555555555555545, oy + assert_equal 70.55555555555554, ow + assert_equal 70.55555555555554, oh + assert_equal "xMidYMid", aspect_ratio_align + assert_equal "meet", aspect_ratio_ms + end + + test "SVG parse_svg_tag_attributes viewBox test" do + datas = [ + '', + '', + ] + + datas.each do |data| + tf = Tempfile.open(['test', '.svg']) do |fp| + fp.puts data + fp + end + pdf = MYPDF.new + ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms = pdf.parse_svg_tag_attributes(tf.path, 0, 0) + assert_equal 0, ox + assert_equal 0, oy + assert_equal 22.577777777777776, ow + assert_equal 28.222222222222218, oh + assert_equal "xMidYMid", aspect_ratio_align + assert_equal "meet", aspect_ratio_ms + end + end +end From b66cb7bb8f51a7cd5c8ecafb80dd4ac0dbec28d2 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Thu, 23 Nov 2023 18:02:26 +0900 Subject: [PATCH 06/18] fix parse_svg_tag_attributes viewBox problem --- lib/rbpdf.rb | 49 ++++++++++++++++++------------- test/rbpdf_image_svg_func_test.rb | 44 +++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index cd62c78..04c70be 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -17072,17 +17072,17 @@ def image_svg(file, x='', y='', w=0, h=0, link='', align='', palign='', border=0 y = @y if y == '' k = @k - ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms = parse_svg_tag_attributes(file, w, h) + width, height, ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms = parse_svg_tag_attributes(file, w, h) # calculate image width and height on document if (w <= 0) && (h <= 0) # convert image size to document unit - w = ow - h = oh + w = width + h = height elsif w <= 0 - w = h * ow / oh + w = h * width / height elsif h <= 0 - h = w * oh / ow + h = w * height / width end # Check whether we need a new page first as this does not fit prev_x = @x @@ -17260,8 +17260,8 @@ def image_svg(file, x='', y='', w=0, h=0, link='', align='', palign='', border=0 def parse_svg_tag_attributes(file, w, h) ox = 0 oy = 0 - ow = w - oh = h + ow = nil + oh = nil aspect_ratio_align = 'xMidYMid' aspect_ratio_ms = 'meet' open(file,'rb') do |f| @@ -17272,18 +17272,6 @@ def parse_svg_tag_attributes(file, w, h) if (res.event_type == :start_element) && (res[0] == 'svg') attribs = res[1] - if attribs['x'] && !attribs['x'].empty? - ox = getHTMLUnitToUnits(attribs['x'], 0, @svgunit, false) - end - if attribs['y'] && !attribs['y'].empty? - oy = getHTMLUnitToUnits(attribs['y'], 0, @svgunit, false) - end - if attribs['width'] && !attribs['width'].empty? - ow = getHTMLUnitToUnits(attribs['width'], 1, @svgunit, false) - end - if attribs['height'] && !attribs['height'].empty? - oh = getHTMLUnitToUnits(attribs['height'], 1, @svgunit, false) - end if attribs['viewBox'] && !attribs['viewBox'].empty? tmp = attribs['viewBox'].strip.split(/[,\s]+/) view_box = [] @@ -17314,12 +17302,33 @@ def parse_svg_tag_attributes(file, w, h) end end end + + if attribs['x'] && !attribs['x'].empty? + ox = getHTMLUnitToUnits(attribs['x'], 0, @svgunit, false) + end + if attribs['y'] && !attribs['y'].empty? + oy = getHTMLUnitToUnits(attribs['y'], 0, @svgunit, false) + end + if attribs['width'] && !attribs['width'].empty? + refsize = (w == 0) ? (ow.nil? ? 1 : ow) : w + w = getHTMLUnitToUnits(attribs['width'], refsize, @svgunit, false) + ow ||= w + elsif !ow.nil? + w = ow + end + if attribs['height'] && !attribs['height'].empty? + refsize = (h == 0) ? (oh.nil? ? 1 : oh) : h + h = getHTMLUnitToUnits(attribs['height'], refsize, @svgunit, false) + oh ||= h + elsif !oh.nil? + h = oh + end break end end end - [ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms] + [w, h, ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms] end private :parse_svg_tag_attributes diff --git a/test/rbpdf_image_svg_func_test.rb b/test/rbpdf_image_svg_func_test.rb index 2f454f3..6ff79d3 100644 --- a/test/rbpdf_image_svg_func_test.rb +++ b/test/rbpdf_image_svg_func_test.rb @@ -12,6 +12,42 @@ def parse_svg_tag_attributes(file, w, h) end end + test "SVG parse_svg_tag_attributes x y (no width height) test" do + tf = Tempfile.open(['test', '.svg']) do |fp| + fp.puts '' + fp + end + + pdf = MYPDF.new + w, h, ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms = pdf.parse_svg_tag_attributes(tf.path, 0, 0) + assert_equal 0, w + assert_equal 0, h + assert_equal 3.5277777777777772, ox + assert_equal 7.0555555555555545, oy + assert_equal nil, ow + assert_equal nil, oh + assert_equal "xMidYMid", aspect_ratio_align + assert_equal "meet", aspect_ratio_ms + end + + test "SVG parse_svg_tag_attributes x y (width height 100%) test" do + tf = Tempfile.open(['test', '.svg']) do |fp| + fp.puts '' + fp + end + + pdf = MYPDF.new + w, h, ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms = pdf.parse_svg_tag_attributes(tf.path, 10, 20) + assert_equal 10.0, w + assert_equal 20.0, h + assert_equal 3.5277777777777772, ox + assert_equal 7.0555555555555545, oy + assert_equal 10.0, ow + assert_equal 20.0, oh + assert_equal "xMidYMid", aspect_ratio_align + assert_equal "meet", aspect_ratio_ms + end + test "SVG parse_svg_tag_attributes x y width height test" do tf = Tempfile.open(['test', '.svg']) do |fp| fp.puts '' @@ -19,7 +55,9 @@ def parse_svg_tag_attributes(file, w, h) end pdf = MYPDF.new - ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms = pdf.parse_svg_tag_attributes(tf.path, 0, 0) + w, h, ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms = pdf.parse_svg_tag_attributes(tf.path, 0, 0) + assert_equal 70.55555555555554, w + assert_equal 70.55555555555554, h assert_equal 3.5277777777777772, ox assert_equal 7.0555555555555545, oy assert_equal 70.55555555555554, ow @@ -40,7 +78,9 @@ def parse_svg_tag_attributes(file, w, h) fp end pdf = MYPDF.new - ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms = pdf.parse_svg_tag_attributes(tf.path, 0, 0) + w, h, ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms = pdf.parse_svg_tag_attributes(tf.path, 0, 0) + assert_equal 22.577777777777776, w + assert_equal 28.222222222222218, h assert_equal 0, ox assert_equal 0, oy assert_equal 22.577777777777776, ow From 3a1d91063b031a7c8b87017ec443524d9fc4dea3 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Fri, 29 Dec 2023 11:20:21 +0900 Subject: [PATCH 07/18] Add empty svg file check. --- lib/rbpdf.rb | 12 ++++++------ test/rbpdf_image_svg_func_test.rb | 10 ++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index 04c70be..ea51a7e 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -17072,6 +17072,7 @@ def image_svg(file, x='', y='', w=0, h=0, link='', align='', palign='', border=0 y = @y if y == '' k = @k + # start parsing an XML SVG header width, height, ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms = parse_svg_tag_attributes(file, w, h) # calculate image width and height on document @@ -17193,6 +17194,7 @@ def image_svg(file, x='', y='', w=0, h=0, link='', align='', palign='', border=0 f = (@h - oy) * @k * (1 - svgscale_y) out(sprintf('%.3f %.3f %.3f %.3f %.3f %.3f cm', svgscale_x, 0, 0, svgscale_y, e + svgoffset_x, f + svgoffset_y)) + # start parsing an XML document open(file,'rb') do |f| parser = REXML::Parsers::PullParser.new(f) while parser.has_next? @@ -17215,12 +17217,6 @@ def image_svg(file, x='', y='', w=0, h=0, link='', align='', palign='', border=0 end end - # start parsing an XML document - #if !xml_parse(@parser, svgdata) - # error_message = sprintf("SVG Error: %s at line %d", xml_error_string(xml_get_error_code(@parser)), xml_get_current_line_number(@parser)) - # error(error_message) - #end - # restore previous graphic state out("#{@epsmarker}Q") # restore graphic vars @@ -17264,12 +17260,14 @@ def parse_svg_tag_attributes(file, w, h) oh = nil aspect_ratio_align = 'xMidYMid' aspect_ratio_ms = 'meet' + svg_check = false open(file,'rb') do |f| parser = REXML::Parsers::PullParser.new(f) while parser.has_next? res = parser.pull if (res.event_type == :start_element) && (res[0] == 'svg') + svg_check = true attribs = res[1] if attribs['viewBox'] && !attribs['viewBox'].empty? @@ -17328,6 +17326,8 @@ def parse_svg_tag_attributes(file, w, h) end end + error("SVG tag not found in SVG file: #{file}") unless svg_check + [w, h, ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms] end private :parse_svg_tag_attributes diff --git a/test/rbpdf_image_svg_func_test.rb b/test/rbpdf_image_svg_func_test.rb index 6ff79d3..01638d4 100644 --- a/test/rbpdf_image_svg_func_test.rb +++ b/test/rbpdf_image_svg_func_test.rb @@ -12,6 +12,16 @@ def parse_svg_tag_attributes(file, w, h) end end + test "SVG parse_svg_tag_attributes empty file test" do + tf = Tempfile.open(['test', '.svg']) do |fp| + fp.puts '' + fp + end + + pdf = MYPDF.new + assert_raise(RBPDFError) {pdf.parse_svg_tag_attributes(tf.path, 0, 0)} + end + test "SVG parse_svg_tag_attributes x y (no width height) test" do tf = Tempfile.open(['test', '.svg']) do |fp| fp.puts '' From aaf4904b663cd30528c6e2455b28494eb62085cb Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Fri, 29 Dec 2023 20:46:03 +0900 Subject: [PATCH 08/18] fix parse_svg_tag_attributes problem on no x y width height viewBox case. --- lib/rbpdf.rb | 15 +++++++++++++-- test/rbpdf_image_svg_func_test.rb | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index ea51a7e..fadc5ed 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -17143,8 +17143,19 @@ def image_svg(file, x='', y='', w=0, h=0, link='', align='', palign='', border=0 # store SVG position and scale factors svgoffset_x = (ximg - ox) * @k svgoffset_y = -(y - oy) * @k - svgscale_x = w / ow - svgscale_y = h / oh + + if ow.nil? + ow = w + svgscale_x = 1 + else + svgscale_x = w / ow + end + if oh.nil? + oh = h + svgscale_y = 1 + else + svgscale_y = h / oh + end # scaling and alignment if aspect_ratio_align != 'none' # store current scaling values diff --git a/test/rbpdf_image_svg_func_test.rb b/test/rbpdf_image_svg_func_test.rb index 01638d4..20ee469 100644 --- a/test/rbpdf_image_svg_func_test.rb +++ b/test/rbpdf_image_svg_func_test.rb @@ -22,6 +22,24 @@ def parse_svg_tag_attributes(file, w, h) assert_raise(RBPDFError) {pdf.parse_svg_tag_attributes(tf.path, 0, 0)} end + test "SVG parse_svg_tag_attributes (no x y width height viewBox) test" do + tf = Tempfile.open(['test', '.svg']) do |fp| + fp.puts '' + fp + end + + pdf = MYPDF.new + w, h, ox, oy, ow, oh, aspect_ratio_align, aspect_ratio_ms = pdf.parse_svg_tag_attributes(tf.path, 0, 0) + assert_equal 0, w + assert_equal 0, h + assert_equal 0, ox + assert_equal 0, oy + assert_equal nil, ow + assert_equal nil, oh + assert_equal "xMidYMid", aspect_ratio_align + assert_equal "meet", aspect_ratio_ms + end + test "SVG parse_svg_tag_attributes x y (no width height) test" do tf = Tempfile.open(['test', '.svg']) do |fp| fp.puts '' From 4acaa4a100589d67745a757c3f0100b88b73c28a Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Sat, 9 Sep 2023 09:31:44 +0900 Subject: [PATCH 09/18] Refactoring of startSVGElementHandler method Separated the parsing of SVG style properties into the get_styling_properties method. --- lib/rbpdf.rb | 74 +++++++++++++++++-------------- test/rbpdf_image_svg_func_test.rb | 23 ++++++++++ 2 files changed, 63 insertions(+), 34 deletions(-) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index fadc5ed..8d64c45 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -18068,41 +18068,9 @@ def startSVGElementHandler(name, attribs, clipping = false) return end - # get styling properties prev_svgstyle = @svgstyles[@svgstyles.size - 1] # previous style - svgstyle = {} # current style - if attribs['style'] - attribs['style'] = ';' + attribs['style'] - end - prev_svgstyle.each do |key, val| - if attribs[key] - if attribs[key] == 'inherit' - svgstyle[key] = val - else - svgstyle[key] = attribs[key] - end - elsif attribs['style'] - # CSS style syntax - if attribs['style'] =~ /[;"\s]#{key}\s*:\s*([^;"\s]*)/mi - if Regexp.last_match[1] == 'inherit' - svgstyle[key] = val - else - svgstyle[key] = Regexp.last_match[1] - end - else - # default value - svgstyle[key] = @svgstyles[0][key] - end - else - if @svginheritprop.include? key - # inherit previous value - svgstyle[key] = val - else - # default value - svgstyle[key] = @svgstyles[0][key] - end - end - end + svgstyle = get_styling_properties(prev_svgstyle, attribs) # get styling properties + # transformation matrix tm = @svgstyles[@svgstyles.size - 1]['transfmatrix'] if attribs['transform'] && !attribs['transform'].empty? @@ -18394,6 +18362,44 @@ def startSVGElementHandler(name, attribs, clipping = false) end end + def get_styling_properties(prev_svgstyle, attribs) + svgstyle = {} # current style + if attribs['style'] + attribs['style'] = ';' + attribs['style'] + end + prev_svgstyle.each do |key, val| + if attribs[key] + if attribs[key] == 'inherit' + svgstyle[key] = val + else + svgstyle[key] = attribs[key] + end + elsif attribs['style'] + # CSS style syntax + if attribs['style'] =~ /[;\s]#{key}\s*:\s*([^;]*)/mi + if Regexp.last_match[1] == 'inherit' + svgstyle[key] = val + else + svgstyle[key] = Regexp.last_match[1] + end + else + # default value + svgstyle[key] = @svgstyles[0][key] + end + else + if @svginheritprop.include? key + # inherit previous value + svgstyle[key] = val + else + # default value + svgstyle[key] = @svgstyles[0][key] + end + end + end + svgstyle + end + private :get_styling_properties + # # Sets the closing SVG element handler function for the XML parser. # [@param string :name] The second parameter, name, contains the name of the element for which this handler is called. If case-folding is in effect for this parser, the element name will be in uppercase letters. diff --git a/test/rbpdf_image_svg_func_test.rb b/test/rbpdf_image_svg_func_test.rb index 20ee469..f4c9be8 100644 --- a/test/rbpdf_image_svg_func_test.rb +++ b/test/rbpdf_image_svg_func_test.rb @@ -10,6 +10,10 @@ class MYPDF < RBPDF def parse_svg_tag_attributes(file, w, h) super end + + def get_styling_properties(prev_svgstyle, attribs) + super + end end test "SVG parse_svg_tag_attributes empty file test" do @@ -117,4 +121,23 @@ def parse_svg_tag_attributes(file, w, h) assert_equal "meet", aspect_ratio_ms end end + + test "SVG get_styling_properties style test" do + datas = [ + {input: {"style"=>"stroke: black; fill: none"}, + output: {"fill"=>"none", "stroke"=>"black"}}, + {input: {"style"=>"stop-color: rgb(83, 25, 113); stop-opacity: 1;"}, + output: {"stop-color"=>"rgb(83, 25, 113)", "stop-opacity"=>"1"}}, + ] + + datas.each do |data| + pdf = MYPDF.new + prev_svgstyle = pdf.instance_variable_get('@svgstyles').last + attrs = data[:input] + svgstyle = pdf.get_styling_properties(prev_svgstyle, attrs) + + diff = svgstyle.to_a - prev_svgstyle.to_a + assert_equal(data[:output], Hash[*diff.flatten]) + end + end end From 5db7103a01cfe3e91992806b35d1c803288db386 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Sat, 26 Aug 2023 23:36:50 +0900 Subject: [PATCH 10/18] svg_path : fix coordinate-pair probrem --- lib/rbpdf.rb | 2 +- test/rbpdf_image_svg_func_test.rb | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index 8d64c45..c8a0973 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -17775,7 +17775,7 @@ def svg_path(d, style='') params = [] if val[1] # get curve parameters - rawparams = val[1].strip.split(/[,\s]+/mi) + rawparams = val[1].strip.scan(/-?[.\d]+/) rawparams.each_with_index do |cp, ck| params[ck] = getHTMLUnitToUnits(cp, 0, @svgunit, false) end diff --git a/test/rbpdf_image_svg_func_test.rb b/test/rbpdf_image_svg_func_test.rb index f4c9be8..69868cc 100644 --- a/test/rbpdf_image_svg_func_test.rb +++ b/test/rbpdf_image_svg_func_test.rb @@ -14,6 +14,10 @@ def parse_svg_tag_attributes(file, w, h) def get_styling_properties(prev_svgstyle, attribs) super end + + def svg_path(d, style='') + super + end end test "SVG parse_svg_tag_attributes empty file test" do @@ -140,4 +144,14 @@ def get_styling_properties(prev_svgstyle, attribs) assert_equal(data[:output], Hash[*diff.flatten]) end end + + test "SVG svg_path test" do + pdf = MYPDF.new + x, y, w, h = pdf.svg_path("M30,1h40l29,29v40l-29,29h-40l-29.0-29v-40z", "D") + + assert_equal 0.35277777777777963, x + assert_equal 0.35277777777777775, y + assert_equal 34.572222222222216, w + assert_equal 34.57222222222222, h + end end From 8c9d99f9c31bd27bd616a234133960fd09a363b6 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Sat, 2 Sep 2023 11:07:47 +0900 Subject: [PATCH 11/18] Added anchor option to Cell and getCellCode method to support SVG text-anchor. [note] text-anchor takes precedence over align. * 'start' : dx = @c_margin x=0 |------------------------------------w------------------------------------| |@c_margin|----------width---------| ABCDEFGHIJKLMNOPQRSTUVWXYZ (LTR) ZYXWVUTSRQPONMLKJIHGFEDCBA (RTL) * 'middle': dx = -width / 2.0 x=0 |--width/2--| |---------width----------| ABCDEFGHIJKLMNOPQRSTUVWXYZ (LTR) ZYXWVUTSRQPONMLKJIHGFEDCBA (RTL) * 'end' : dx = - width - @c_margin x=0 |@c_margin| |----------width---------| ABCDEFGHIJKLMNOPQRSTUVWXYZ (LTR) ZWXWVUTSRQPONMLKJIHGFEDCBA (RTL) --- lib/rbpdf.rb | 33 +++++++++++++++++++++--- test/rbpdf_cell_test.rb | 56 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index c8a0973..26d9484 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -3451,11 +3451,17 @@ def removeSHY(txt='') # * T : top # * M : middle # * B : bottom + # [@param string :anchor] + # The text-anchor attribute is used to align (start-, middle- or end-alignment) a string. + # * empty string: no anchor mode (default value) + # * start: The rendered characters are aligned such that the start of the text string is at the initial current text position. + # * middle: The rendered characters are aligned such that the middle of the text string is at the current text position. + # * end: The rendered characters are shifted such that the end of the resulting rendered text. # [@access public] # [@since 1.0] # [@see] SetFont(), SetDrawColor(), SetFillColor(), SetTextColor(), SetLineWidth(), AddLink(), Ln(), MultiCell(), Write(), SetAutoPageBreak() # - def Cell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link=nil, stretch=0, ignore_min_height=false, calign='T', valign='M') + def Cell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link=nil, stretch=0, ignore_min_height=false, calign='T', valign='M', anchor='') if !ignore_min_height min_cell_height = @font_size * @cell_height_ratio if h < min_cell_height @@ -3463,7 +3469,7 @@ def Cell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link=nil, stretch=0, end end checkPageBreak(h) - out(getCellCode(w, h, txt, border, ln, align, fill, link, stretch, ignore_min_height, calign, valign)) + out(getCellCode(w, h, txt, border, ln, align, fill, link, stretch, ignore_min_height, calign, valign, anchor)) rescue => err Error('Cell Error.', err) end @@ -3519,11 +3525,17 @@ def Cell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link=nil, stretch=0, # * T : top # * M : middle # * B : bottom + # [@param string :anchor] + # The text-anchor attribute is used to align (start-, middle- or end-alignment) a string. + # * empty string: no anchor mode (default value) + # * start: The rendered characters are aligned such that the start of the text string is at the initial current text position. + # * middle: The rendered characters are aligned such that the middle of the text string is at the current text position. + # * end: The rendered characters are shifted such that the end of the resulting rendered text. # [@access protected] # [@since 1.0] # [@see] Cell() # - def getCellCode(w, h=0, txt='', border=0, ln=0, align='', fill=0, link=nil, stretch=0, ignore_min_height=false, calign='T', valign='M') + def getCellCode(w, h=0, txt='', border=0, ln=0, align='', fill=0, link=nil, stretch=0, ignore_min_height=false, calign='T', valign='M', anchor='') txt = '' if txt.nil? rs = "" # string to be returned txt = removeSHY(txt) @@ -3781,6 +3793,16 @@ def getCellCode(w, h=0, txt='', border=0, ln=0, align='', fill=0, link=nil, stre dx = @c_margin end + # [note] text-anchor takes precedence over align. + case anchor + when 'start' + dx = @c_margin + when 'middle' + dx = -width / 2.0 + when 'end' + dx = - width - @c_margin + end + if @rtl xdx = @x - dx - width else @@ -18347,6 +18369,7 @@ def startSVGElementHandler(name, attribs, clipping = false) svg_transform(tm) obstyle = setSVGStyles(svgstyle, prev_svgstyle, x, y, 1, 1) set_xy(x, y, true) + @svgstyles.push(svgstyle) when 'use' # use if attribs['xlink:href'] use = @svgdefs[attribs['xlink:href'][1..-1]] @@ -18417,8 +18440,10 @@ def endSVGElementHandler(name) @svgstyles.pop stop_transform() when 'text', 'tspan' + anchor = @svgstyles.last['text-anchor'] # print text - cell(0, 0, @svgtext.strip, 0, 0, '', 0, '', 0, false, 'L', 'T') + cell(0, 0, @svgtext.strip, 0, 0, '', 0, '', 0, false, 'L', 'T', anchor) + @svgstyles.pop stop_transform() end end diff --git a/test/rbpdf_cell_test.rb b/test/rbpdf_cell_test.rb index 5a11407..cacecaa 100644 --- a/test/rbpdf_cell_test.rb +++ b/test/rbpdf_cell_test.rb @@ -1,4 +1,4 @@ -# Copyright (c) 2011-2018 NAITOH Jun +# Copyright (c) 2011-2023 NAITOH Jun # Released under the MIT license # http://www.opensource.org/licenses/MIT @@ -71,6 +71,60 @@ class RbpdfTest < Test::Unit::TestCase # ET end + test "getCellCode text-anchor start test" do + pdf = RBPDF.new('P', 'mm', 'A4', true, "UTF-8", true) + pdf.add_page() + content = [] + + contents = pdf.send(:getCellCode, 10, 10, 'abc', 'LTRB',0 , '', 0, nil, 0, false, '', '', 'start') + contents.each_line {|line| content.push line.chomp } + + assert_equal 2, content.length + assert_equal "0.57 w 0 J 0 j [] 0 d 0 G 0 g", content[0] + assert_match(/BT 31.1[89] 795.17 Td 0 Tr 0.00 w \[\(abc\)\] TJ ET/, content[1]) + # BT + # 31.19 795.17 Td + # 0 Tr 0.00 w + # [(abc)] TJ + # ET + end + + test "getCellCode text-anchor middle test" do + pdf = RBPDF.new('P', 'mm', 'A4', true, "UTF-8", true) + pdf.add_page() + content = [] + + contents = pdf.send(:getCellCode, 10, 10, 'abc', 'LTRB',0 , '', 0, nil, 0, false, '', '', 'middle') + contents.each_line {|line| content.push line.chomp } + + assert_equal 2, content.length + assert_equal "0.57 w 0 J 0 j [] 0 d 0 G 0 g", content[0] + assert_match(/BT 18.68 795.17 Td 0 Tr 0.00 w \[\(abc\)\] TJ ET/, content[1]) + # BT + # 18.68 795.17 Td + # 0 Tr 0.00 w + # [(abc)] TJ + # ET + end + + test "getCellCode text-anchor end test" do + pdf = RBPDF.new('P', 'mm', 'A4', true, "UTF-8", true) + pdf.add_page() + content = [] + + contents = pdf.send(:getCellCode, 10, 10, 'abc', 'LTRB',0 , '', 0, nil, 0, false, '', '', 'end') + contents.each_line {|line| content.push line.chomp } + + assert_equal 2, content.length + assert_equal "0.57 w 0 J 0 j [] 0 d 0 G 0 g", content[0] + assert_match(/BT 6.17 795.17 Td 0 Tr 0.00 w \[\(abc\)\] TJ ET/, content[1]) + # BT + # 6.17 795.17 Td + # 0 Tr 0.00 w + # [(abc)] TJ + # ET + end + test "getCellCode link url test" do pdf = RBPDF.new('P', 'mm', 'A4', true, "UTF-8", true) pdf.add_page() From 6da5986a6e597fc600982be662419b4d3e684740 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Sat, 2 Sep 2023 22:03:34 +0900 Subject: [PATCH 12/18] fix defalut value line_style={}, border_style={}, fill_color=[], circle_outLine_style={}, circle_fill_color=[] - Rect,RoundedRect,RoundedRect - Curve,Ellipse,Circle - PolyLine,Polygon,Polygon,RegularPolygon,StarPolygon --- .../app/controllers/example012_controller.rb | 8 +- lib/rbpdf.rb | 95 +++++++++--------- test/rbpdf_graphic_func_test.rb | 99 +++++++++++++++++++ test/rbpdf_image_svg_test.rb | 61 ++++++++++++ 4 files changed, 210 insertions(+), 53 deletions(-) create mode 100644 test/rbpdf_image_svg_test.rb diff --git a/example/rails/app/controllers/example012_controller.rb b/example/rails/app/controllers/example012_controller.rb index bcf44c3..9a36bad 100644 --- a/example/rails/app/controllers/example012_controller.rb +++ b/example/rails/app/controllers/example012_controller.rb @@ -54,7 +54,7 @@ def index style = {'width' => 0.5, 'cap' => 'butt', 'join' => 'miter', 'dash' => '10,20,5,10', 'phase' => 10, 'color' => [255, 0, 0]} style2 = {'width' => 0.5, 'cap' => 'butt', 'join' => 'miter', 'dash' => 0, 'color' => [255, 0, 0]} style3 = {'width' => 1, 'cap' => 'round', 'join' => 'round', 'dash' => '2,10', 'color' => [255, 0, 0]} - style4 = {'L' => 0, + style4 = {'L' => nil, 'T' => {'width' => 0.25, 'cap' => 'butt', 'join' => 'miter', 'dash' => '20,10', 'phase' => 10, 'color' => [100, 100, 255]}, 'R' => {'width' => 0.50, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => [50, 50, 127]}, 'B' => {'width' => 0.75, 'cap' => 'square', 'join' => 'miter', 'dash' => '30,10,5,10'}} @@ -107,7 +107,7 @@ def index # Polygonal Line pdf.set_line_style({'width' => 0.5, 'cap' => 'butt', 'join' => 'miter', 'dash' => 0, 'color' => [0, 0, 164]}) - pdf.poly_line([80,165,90,160,100,165,110,160,120,165,130,160,140,165], 'D', nil, nil) + pdf.poly_line([80,165,90,160,100,165,110,160,120,165,130,160,140,165], 'D') # Regular polygon pdf.text(5, 169, 'Regular polygon examples') @@ -124,8 +124,8 @@ def index pdf.set_line_style(style5) pdf.star_polygon(20, 230, 15, 20, 3, 0, 1, 'F') pdf.star_polygon(55, 230, 15, 12, 5) - pdf.star_polygon(55, 230, 7, 12, 5, 45, 0, 'DF', {'all' => style7}, [220, 220, 200], 'F', [255, 200, 200]) - pdf.star_polygon(90, 230, 15, 20, 6, 0, 1, 'DF', {'all' => style5}, [220, 220, 200], 'F', [255, 200, 200]) + pdf.star_polygon(55, 230, 7, 12, 5, 45, 0, 'DF', {'all' => style7}, [220, 220, 200], 'F', nil, [255, 200, 100]) + pdf.star_polygon(90, 230, 15, 20, 6, 0, 1, 'DF', {'all' => style5}, [220, 220, 200], 'F', nil, [255, 200, 100]) pdf.star_polygon(125, 230, 15, 5, 2, 30, 1, nil, {'all' => style5}, nil, nil, style6) pdf.star_polygon(160, 230, 15, 10, 3) pdf.star_polygon(160, 230, 7, 50, 26) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index 26d9484..045794f 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -9190,31 +9190,28 @@ def Line(x1, y1, x2, y2, style={}) # [@param float :y] Ordinate of upper-left corner (or upper-right corner for RTL language). # [@param float :w] Width. # [@param float :h] Height. - # [@param string :style] - # Style of rendering. See the getPathPaintOperator() function for more information. + # [@param string :style] Style of rendering. See the getPathPaintOperator() function for more information. + # [@param hash :border_style] Border style of rectangle. Hash with keys among the following: # * all: Line style of all borders. Array like for {@link SetLineStyle SetLineStyle}. # * L, T, R, B or combinations: Line style of left, top, right or bottom border. Array like for {@link SetLineStyle SetLineStyle}. - # If a key is not present or is null, not draws the border. Default value: default line style (empty array). - # [@param array :border_style] Border style of rectangle. Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty array). + # If a key is not present or is null, not draws the border. Default value: default line style (empty Hash). # [@param array :fill_color] Fill color. Format: array(GREY) or array(R,G,B) or array(C,M,Y,K). Default value: default color (empty array). # [@access public] # [@since 1.0] # [@see] SetLineStyle() # - def Rect(x, y, w, h, style='', border_style={}, fill_color={}) - if style.index('F') != nil and !fill_color.empty? + def Rect(x, y, w, h, style='', border_style={}, fill_color=[]) + if !style.index('F').nil? && fill_color && !fill_color.empty? SetFillColorArray(fill_color) end op = getPathPaintOperator(style) - if !border_style or !border_style['all'].nil? - if !border_style['all'].nil? and border_style['all'] - SetLineStyle(border_style['all']) - border_style = {} - end + if border_style && border_style['all'] + SetLineStyle(border_style['all']) + border_style = {} end outRect(x, y, w, h, op) - if border_style + if border_style && !border_style.empty? border_style2 = {} border_style.each { |line, value| length = line.length @@ -9252,18 +9249,18 @@ def Rect(x, y, w, h, style='', border_style={}, fill_color={}) # [@param float :x3] Abscissa of end point. # [@param float :y3] Ordinate of end point. # [@param string :style] Style of rendering. See the getPathPaintOperator() function for more information. - # [@param array :line_style] Line style of curve. Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty array). + # [@param hash :line_style] Line style of curve. Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty Hash). # [@param array :fill_color] Fill color. Format: array(GREY) or array(R,G,B) or array(C,M,Y,K). Default value: default color (empty array). # [@access public] # [@see] SetLineStyle() # [@since 2.1.000 (2008-01-08)] # - def Curve(x0, y0, x1, y1, x2, y2, x3, y3, style='', line_style=nil, fill_color=nil) - if style and (style.index('F') != nil) and fill_color + def Curve(x0, y0, x1, y1, x2, y2, x3, y3, style='', line_style={}, fill_color=[]) + if style && !style.index('F').nil? && fill_color && !fill_color.empty? SetFillColorArray(fill_color) end op = getPathPaintOperator(style) - if line_style + if line_style && !line_style.empty? SetLineStyle(line_style) end outPoint(x0, y0) @@ -9283,19 +9280,19 @@ def Curve(x0, y0, x1, y1, x2, y2, x3, y3, style='', line_style=nil, fill_color=n # [@param float :astart] Angle start of draw line. Default value: 0. # [@param float :afinish] Angle finish of draw line. Default value: 360. # [@param string :style] Style of rendering. See the getPathPaintOperator() function for more information. - # [@param array :line_style] Line style of ellipse. Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty array). + # [@param hash :line_style] Line style of ellipse. Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty Hash). # [@param array :fill_color] Fill color. Format: array(GREY) or array(R,G,B) or array(C,M,Y,K). Default value: default color (empty array). # [@param integer :nc] Number of curves used to draw a 90 degrees portion of ellipse. # [@author] Nicola Asuni # [@access public] # [@since 2.1.000 (2008-01-08)] # - def Ellipse(x0, y0, rx, ry='', angle=0, astart=0, afinish=360, style='', line_style=nil, fill_color=nil, nc=2) + def Ellipse(x0, y0, rx, ry='', angle=0, astart=0, afinish=360, style='', line_style={}, fill_color=[], nc=2) style = '' if style.nil? if empty_string(ry) or (ry == 0) ry = rx end - if (nil != style.index('F')) and fill_color + if !style.index('F').nil? && fill_color && !fill_color.empty? SetFillColorArray(fill_color) end op = getPathPaintOperator(style) @@ -9420,13 +9417,13 @@ def outellipticalarc(xc, yc, rx, ry, xang=0, angs=0, angf=360, pie=false, nc=2) # [@param float :angstr] Angle start of draw line. Default value: 0. # [@param float :angend] Angle finish of draw line. Default value: 360. # [@param string :style] Style of rendering. See the getPathPaintOperator() function for more information. - # [@param array :line_style] Line style of circle. Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty array). + # [@param hash :line_style] Line style of circle. Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty Hash). # [@param array :fill_color] Fill color. Format: array(red, green, blue). Default value: default color (empty array). # [@param integer :nc] Number of curves used to draw a 90 degrees portion of circle. # [@access public] # [@since 2.1.000 (2008-01-08)] # - def Circle(x0, y0, r, angstr=0, angend=360, style='', line_style=nil, fill_color=nil, nc=2) + def Circle(x0, y0, r, angstr=0, angend=360, style='', line_style={}, fill_color=[], nc=2) Ellipse(x0, y0, r, r, 0, angstr, angend, style, line_style, fill_color, nc) end alias_method :circle, :Circle @@ -9435,17 +9432,17 @@ def Circle(x0, y0, r, angstr=0, angend=360, style='', line_style=nil, fill_color # Draws a polygonal line # [@param array :p] Points 0 to (:np - 1). Array with values (x0, y0, x1, y1,..., x(np-1), y(np - 1)) # [@param string :style] Style of rendering. See the getPathPaintOperator() function for more information. - # [@param array :line_style] - # Line style of polygon. Array with keys among the following: + # [@param hash :line_style] + # Line style of polygon. Hash with keys among the following: # * all: Line style of all lines. Array like for {@link SetLineStyle SetLineStyle}. # * 0 to (:np - 1): Line style of each line. Array like for {@link SetLineStyle SetLineStyle}. - # If a key is not present or is null, not draws the line. Default value is default line style (empty array). + # If a key is not present or is null, not draws the line. Default value is default line style (empty Hash). # [@param array :fill_color] Fill color. Format: array(GREY) or array(R,G,B) or array(C,M,Y,K). Default value: default color (empty array). # [@param boolean :closed] if true the polygon is closes, otherwise will remain open # [@access public] # [@since 4.8.003 (2009-09-15)] # - def PolyLine(p, style='', line_style=nil, fill_color=nil) + def PolyLine(p, style='', line_style={}, fill_color=[]) Polygon(p, style, line_style, fill_color, false) end alias_method :poly_line, :PolyLine @@ -9454,17 +9451,17 @@ def PolyLine(p, style='', line_style=nil, fill_color=nil) # Draws a polygon. # [@param array :p] Points 0 to (np - 1). Array with values (x0, y0, x1, y1,..., x(np-1), y(np - 1)) # [@param string :style] Style of rendering. See the getPathPaintOperator() function for more information. - # [@param array :line_style] - # Line style of polygon. Array with keys among the following: + # [@param hash :line_style] + # Line style of polygon. Hash with keys among the following: # * all: Line style of all lines. Array like for {@link SetLineStyle SetLineStyle}. # * 0 to (:np - 1): Line style of each line. Array like for {@link SetLineStyle SetLineStyle}. - # If a key is not present or is null, not draws the line. Default value is default line style (empty array). + # If a key is not present or is null, not draws the line. Default value is default line style (empty Hash). # [@param array :fill_color] Fill color. Format: array(GREY) or array(R,G,B) or array(C,M,Y,K). Default value: default color (empty array). # [@param boolean :closed] if true the polygon is closes, otherwise will remain open # [@access public] # [@since 2.1.000 (2008-01-08)] # - def Polygon(p, style='', line_style=nil, fill_color=nil, closed=true) + def Polygon(p, style='', line_style={}, fill_color=[], closed=true) style = '' if style.nil? nc = p.length # number of coordinates np = nc / 2 # number of points @@ -9483,15 +9480,15 @@ def Polygon(p, style='', line_style=nil, fill_color=nil, closed=true) end nc += 4 end - if (nil != style.index('F')) and fill_color + if !style.index('F').nil? && fill_color && !fill_color.empty? SetFillColorArray(fill_color) end op = getPathPaintOperator(style) if op == 'f' - line_style = [] + line_style = {} end draw = true - if line_style + if line_style && !line_style.empty? if line_style.is_a? Hash and line_style['all'] SetLineStyle(line_style['all']) else @@ -9548,11 +9545,11 @@ def Polygon(p, style='', line_style=nil, fill_color=nil, closed=true) # [@param float :angle] Angle oriented (anti-clockwise). Default value: 0. # [@param boolean :draw_circle] Draw inscribed circle or not. Default value: false. # [@param string :style] Style of rendering. See the getPathPaintOperator() function for more information. - # [@param array :line_style] - # Line style of polygon sides. Array with keys among the following: + # [@param hash :line_style] + # Line style of polygon sides. Hash with keys among the following: # * all: Line style of all sides. Array like for {@link SetLineStyle SetLineStyle}. # * 0 to (:ns - 1): Line style of each side. Array like for {@link SetLineStyle SetLineStyle}. - # If a key is not present or is null, not draws the side. Default value is default line style (empty array). + # If a key is not present or is null, not draws the side. Default value is default line style (empty Hash). # [@param array :fill_color] Fill color. Format: array(red, green, blue). Default value: default color (empty array). # [@param string :circle_style] # Style of rendering of inscribed circle (if draws). Possible values are: @@ -9561,12 +9558,12 @@ def Polygon(p, style='', line_style=nil, fill_color=nil, closed=true) # * DF or FD: Draw and fill. # * CNZ: Clipping mode (using the even-odd rule to determine which regions lie inside the clipping path). # * CEO: Clipping mode (using the nonzero winding number rule to determine which regions lie inside the clipping path). - # [@param array :circle_outLine_style] Line style of inscribed circle (if draws). Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty array). + # [@param hash :circle_outLine_style] Line style of inscribed circle (if draws). Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty Hash). # [@param array :circle_fill_color] Fill color of inscribed circle (if draws). Format: array(red, green, blue). Default value: default color (empty array). # [@access public] # [@since 2.1.000 (2008-01-08)] # - def RegularPolygon(x0, y0, r, ns, angle=0, draw_circle=false, style='', line_style=nil, fill_color=nil, circle_style='', circle_outLine_style=nil, circle_fill_color=nil) + def RegularPolygon(x0, y0, r, ns, angle=0, draw_circle=false, style='', line_style={}, fill_color=[], circle_style='', circle_outLine_style={}, circle_fill_color=[]) draw_circle = false if draw_circle == 0 if 3 > ns ns = 3 @@ -9595,11 +9592,11 @@ def RegularPolygon(x0, y0, r, ns, angle=0, draw_circle=false, style='', line_sty # [@param float :angle] Angle oriented (anti-clockwise). Default value: 0. # [@param boolean :draw_circle] Draw inscribed circle or not. Default value is false. # [@param string :style] Style of rendering. See the getPathPaintOperator() function for more information. - # [@param array :line_style] - # Line style of polygon sides. Array with keys among the following: + # [@param hash :line_style] + # Line style of polygon sides. Hash with keys among the following: # * all: Line style of all sides. Array like for {@link SetLineStyle SetLineStyle}. # * 0 to (n - 1): Line style of each side. Array like for {@link SetLineStyle SetLineStyle}. - # If a key is not present or is null, not draws the side. Default value is default line style (empty array). + # If a key is not present or is null, not draws the side. Default value is default line style (empty Hash). # [@param array :fill_color ]Fill color. Format: array(red, green, blue). Default value: default color (empty array). # [@param string :circle_style] # Style of rendering of inscribed circle (if draws). Possible values are: @@ -9608,12 +9605,12 @@ def RegularPolygon(x0, y0, r, ns, angle=0, draw_circle=false, style='', line_sty # * DF or FD: Draw and fill. # * CNZ: Clipping mode (using the even-odd rule to determine which regions lie inside the clipping path). # * CEO: Clipping mode (using the nonzero winding number rule to determine which regions lie inside the clipping path). - # [@param array :circle_outLine_style] Line style of inscribed circle (if draws). Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty array). + # [@param hash :circle_outLine_style] Line style of inscribed circle (if draws). Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty Hash). # [@param array :circle_fill_color] Fill color of inscribed circle (if draws). Format: array(red, green, blue). Default value: default color (empty array). # [@access public] # [@since 2.1.000 (2008-01-08)] # - def StarPolygon(x0, y0, r, nv, ng, angle=0, draw_circle=false, style='', line_style=nil, fill_color=nil, circle_style='', circle_outLine_style=nil, circle_fill_color=nil) + def StarPolygon(x0, y0, r, nv, ng, angle=0, draw_circle=false, style='', line_style={}, fill_color=[], circle_style='', circle_outLine_style={}, circle_fill_color=[]) draw_circle = false if draw_circle == 0 if nv < 2 nv = 2 @@ -9653,12 +9650,12 @@ def StarPolygon(x0, y0, r, nv, ng, angle=0, draw_circle=false, style='', line_st # [@param float :r] the radius of the circle used to round off the corners of the rectangle. # [@param string :round_corner] Draws rounded corner or not. String with a 0 (not rounded i-corner) or 1 (rounded i-corner) in i-position. Positions are, in order and begin to 0: top left, top right, bottom right and bottom left. Default value: all rounded corner ("1111"). # [@param string :style] Style of rendering. See the getPathPaintOperator() function for more information. - # [@param array :border_style] Border style of rectangle. Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty array). + # [@param hash :border_style] Border style of rectangle. Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty hash). # [@param array :fill_color] Fill color. Format: array(GREY) or array(R,G,B) or array(C,M,Y,K). Default value: default color (empty array). # [@access public] # [@since 2.1.000 (2008-01-08)] # - def RoundedRect(x, y, w, h, r, round_corner='1111', style='', border_style=nil, fill_color=nil) + def RoundedRect(x, y, w, h, r, round_corner='1111', style='', border_style={}, fill_color=[]) RoundedRectXY(x, y, w, h, r, r, round_corner, style, border_style, fill_color) end alias_method :rounded_rect, :RoundedRect @@ -9673,12 +9670,12 @@ def RoundedRect(x, y, w, h, r, round_corner='1111', style='', border_style=nil, # [@param float :ry] the y-axis radius of the ellipse used to round off the corners of the rectangle. # [@param string :round_corner] Draws rounded corner or not. String with a 0 (not rounded i-corner) or 1 (rounded i-corner) in i-position. Positions are, in order and begin to 0: top left, top right, bottom right and bottom left. Default value: all rounded corner ("1111"). # [@param string :style] Style of rendering. See the getPathPaintOperator() function for more information. - # [@param array :border_style] Border style of rectangle. Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty array). + # [@param hash :border_style] Border style of rectangle. Array like for {@link SetLineStyle SetLineStyle}. Default value: default line style (empty hash). # [@param array :fill_color] Fill color. Format: array(GREY) or array(R,G,B) or array(C,M,Y,K). Default value: default color (empty array). # [@access public] # [@since 4.9.019 (2010-04-22)] # - def RoundedRectXY(x, y, w, h, rx, ry, round_corner='1111', style='', border_style=nil, fill_color=nil) + def RoundedRectXY(x, y, w, h, rx, ry, round_corner='1111', style='', border_style={}, fill_color=[]) style = '' if style.nil? if (round_corner == '0000') or ((rx == ry) and (rx == 0)) # Not rounded @@ -9686,14 +9683,14 @@ def RoundedRectXY(x, y, w, h, rx, ry, round_corner='1111', style='', border_styl return end # Rounded - if (nil != style.index('F')) and fill_color + if !style.index('F').nil? && fill_color && !fill_color.empty? SetFillColorArray(fill_color) end op = getPathPaintOperator(style) if op == 'f' - border_style = [] + border_style = {} end - if border_style + unless border_style.empty? SetLineStyle(border_style) end myArc = 4 / 3.0 * (::Math.sqrt(2) - 1) diff --git a/test/rbpdf_graphic_func_test.rb b/test/rbpdf_graphic_func_test.rb index 3e81bd7..8486413 100644 --- a/test/rbpdf_graphic_func_test.rb +++ b/test/rbpdf_graphic_func_test.rb @@ -77,4 +77,103 @@ def getPageBuffer(page) assert_equal "226.77 756.85 l", content[before_size + 6] # outLine(x2, y2) assert_equal "S", content[before_size + 7] end + + test "rect basic test" do + pdf = MYPDF.new + pdf.add_page() + page = pdf.get_page + contents = pdf.getPageBuffer(page) + before_size = contents.split("\n").size + pdf.putshaders + style = { 'L' => nil, 'T' => nil, 'R' => nil, 'B' => nil } + pdf.rect(30, 40, 60, 60, '', style) + + content = [] + contents = pdf.getPageBuffer(page) + contents.each_line {|line| content.push line.chomp } + + assert_equal "85.04 728.50 170.08 -170.08 re S", content[before_size] # outRect(x, y, w, h, op) + end + + test "rect with style & fill_color test" do + pdf = MYPDF.new + pdf.add_page() + page = pdf.get_page + contents = pdf.getPageBuffer(page) + before_size = contents.split("\n").size + pdf.putshaders + pdf.rect(30, 40, 60, 60, 'F', {}, [220, 220, 200]) + + content = [] + contents = pdf.getPageBuffer(page) + contents.each_line {|line| content.push line.chomp } + assert_equal "0.863 0.863 0.784 rg", content[before_size] # SetFillColorArray(fill_color) + assert_equal "85.04 728.50 170.08 -170.08 re f", content[before_size + 1] # outRect(x, y, w, h, op) + end + + test "rect with border_style 'all' test" do + pdf = MYPDF.new + pdf.add_page() + page = pdf.get_page + contents = pdf.getPageBuffer(page) + before_size = contents.split("\n").size + pdf.putshaders + + style = {'width' => 0.1, 'cap' => 'square', 'join' => 'round', 'dash' => 0, 'color' => [0, 64, 128]} + pdf.rect(30, 40, 60, 60, '', { 'all' => style }) + + content = [] + contents = pdf.getPageBuffer(page) + contents.each_line {|line| content.push line.chomp } + + # set_line_style start + assert_equal "0.28 w", content[before_size] # 'width' => 0.1 + assert_equal "2 J", content[before_size + 1] # 'cap' => 'square' + assert_equal "1 j", content[before_size + 2] # 'join' => 'round' + assert_equal "[] 0.00 d", content[before_size + 3] # 'dash' => 0 + assert_equal "0.000 0.251 0.502 RG ", content[before_size + 4] # 'color' => [0, 255, 0] + # set_line_style end + + assert_equal "85.04 728.50 170.08 -170.08 re S", content[before_size + 5] # outRect(x, y, w, h, op) + end + + test "rect with border_style 'LTRB' test" do + pdf = MYPDF.new + pdf.add_page() + page = pdf.get_page + contents = pdf.getPageBuffer(page) + before_size = contents.split("\n").size + pdf.putshaders + + style = { 'L' => {'width' => 0.1}, 'T' => {'width' => 0.1}, 'R' => {'width' => 0.1}, 'B' => {'width' => 0.1} } + pdf.rect(30, 40, 60, 60, '', style) + + content = [] + contents = pdf.getPageBuffer(page) + contents.each_line {|line| content.push line.chomp } + + assert_equal "85.04 728.50 170.08 -170.08 re S", content[before_size] # outRect(x, y, w, h, op) + + # line start + assert_equal "0.28 w", content[before_size + 1] # 'width' => 0.1 + assert_equal "85.04 728.50 m", content[before_size + 2] # outPoint(x1, y1) + assert_equal "85.04 558.43 l", content[before_size + 3] # outLine(x2, y2) + assert_equal "S", content[before_size + 4] + + assert_equal "0.28 w", content[before_size + 5] # 'width' => 0.1 + assert_equal "85.04 728.50 m", content[before_size + 6] # outPoint(x1, y1) + assert_equal "255.12 728.50 l", content[before_size + 7] # outLine(x2, y2) + assert_equal "S", content[before_size + 8] + + assert_equal "0.28 w", content[before_size + 9] # 'width' => 0.1 + assert_equal "255.12 728.50 m", content[before_size + 10] # outPoint(x1, y1) + assert_equal "255.12 558.43 l", content[before_size + 11] # outLine(x2, y2) + assert_equal "S", content[before_size + 12] + + assert_equal "0.28 w", content[before_size + 13] # 'width' => 0.1 + assert_equal "85.04 558.43 m", content[before_size + 14] # outPoint(x1, y1) + assert_equal "255.12 558.43 l", content[before_size + 15] # outLine(x2, y2) + assert_equal "S", content[before_size + 16] + # line end + end end diff --git a/test/rbpdf_image_svg_test.rb b/test/rbpdf_image_svg_test.rb new file mode 100644 index 0000000..f93fb2d --- /dev/null +++ b/test/rbpdf_image_svg_test.rb @@ -0,0 +1,61 @@ +# coding: ASCII-8BIT +# Copyright (c) 2011-2023 NAITOH Jun +# Released under the MIT license +# http://www.opensource.org/licenses/MIT + +require 'test_helper' + +class RbpdfTest < Test::Unit::TestCase + class MYPDF < RBPDF + def startSVGElementHandler(name, attribs, clipping = false) + super + end + end + + test "SVG basic test" do + pdf = RBPDF.new + pdf.add_page + img_file = File.join(File.dirname(__FILE__), 'testsvg.svg') + + pdf.image_svg(img_file, 15, 30, 0, 0, '', '', '', 1, false) + + no = pdf.get_num_pages + assert_equal 1, no + end + + test "SVG basic rect test" do + tf = Tempfile.open(['test', '.svg']) do |fp| + fp.puts ' + + ' + fp + end + + pdf = RBPDF.new + pdf.image_svg(tf.path, 15, 30, 0, 0, '', '', '', 1, false) + end + + test "SVG basic polyline & viewBox test" do + tf = Tempfile.open(['test', '.svg']) do |fp| + fp.puts ' + + ' + fp + end + + pdf = RBPDF.new + pdf.image_svg(tf.path, 15, 30, 0, 0, '', '', '', 1, false) + end + + test "SVG basic circle & viewBox test" do + tf = Tempfile.open(['test', '.svg']) do |fp| + fp.puts ' + + ' + fp + end + + pdf = RBPDF.new + pdf.image_svg(tf.path, 15, 30, 0, 0, '', '', '', 1, false) + end +end From c6f543949ac3d752eb8a247dc441b38795a2e1d4 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Wed, 6 Sep 2023 00:16:51 +0900 Subject: [PATCH 13/18] startSVGElementHandler : fixed problem with svg styles not being applied to polylines. --- lib/rbpdf.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index 045794f..1b41197 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -18304,7 +18304,7 @@ def startSVGElementHandler(name, attribs, clipping = false) start_transform() svg_transform(tm) obstyle = setSVGStyles(svgstyle, prev_svgstyle, x, y, w, h, 'poly_line', [p, 'CNZ']) - poly_line(p, 'D') + poly_line(p, obstyle) stop_transform() else # polygon if clipping From 22c019150e455d54e4bb9a02b47a7e5c2a971615 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Sun, 10 Sep 2023 15:38:02 +0900 Subject: [PATCH 14/18] add rounded_rect & polygon test --- lib/rbpdf.rb | 8 ++-- test/rbpdf_graphic_func_test.rb | 83 +++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index 1b41197..f40fb72 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -9432,9 +9432,9 @@ def Circle(x0, y0, r, angstr=0, angend=360, style='', line_style={}, fill_color= # Draws a polygonal line # [@param array :p] Points 0 to (:np - 1). Array with values (x0, y0, x1, y1,..., x(np-1), y(np - 1)) # [@param string :style] Style of rendering. See the getPathPaintOperator() function for more information. - # [@param hash :line_style] + # [@param hash or array :line_style] # Line style of polygon. Hash with keys among the following: - # * all: Line style of all lines. Array like for {@link SetLineStyle SetLineStyle}. + # * all: Line style of all lines. Hash like for {@link SetLineStyle SetLineStyle}. # * 0 to (:np - 1): Line style of each line. Array like for {@link SetLineStyle SetLineStyle}. # If a key is not present or is null, not draws the line. Default value is default line style (empty Hash). # [@param array :fill_color] Fill color. Format: array(GREY) or array(R,G,B) or array(C,M,Y,K). Default value: default color (empty array). @@ -9451,9 +9451,9 @@ def PolyLine(p, style='', line_style={}, fill_color=[]) # Draws a polygon. # [@param array :p] Points 0 to (np - 1). Array with values (x0, y0, x1, y1,..., x(np-1), y(np - 1)) # [@param string :style] Style of rendering. See the getPathPaintOperator() function for more information. - # [@param hash :line_style] + # [@param hash or array :line_style] # Line style of polygon. Hash with keys among the following: - # * all: Line style of all lines. Array like for {@link SetLineStyle SetLineStyle}. + # * all: Line style of all lines. Hash like for {@link SetLineStyle SetLineStyle}. # * 0 to (:np - 1): Line style of each line. Array like for {@link SetLineStyle SetLineStyle}. # If a key is not present or is null, not draws the line. Default value is default line style (empty Hash). # [@param array :fill_color] Fill color. Format: array(GREY) or array(R,G,B) or array(C,M,Y,K). Default value: default color (empty array). diff --git a/test/rbpdf_graphic_func_test.rb b/test/rbpdf_graphic_func_test.rb index 8486413..58b5fd3 100644 --- a/test/rbpdf_graphic_func_test.rb +++ b/test/rbpdf_graphic_func_test.rb @@ -176,4 +176,87 @@ def getPageBuffer(page) assert_equal "S", content[before_size + 16] # line end end + + test "rounded_rect basic test" do + pdf = MYPDF.new + pdf.add_page() + page = pdf.get_page + contents = pdf.getPageBuffer(page) + before_size = contents.split("\n").size + pdf.putshaders + style = { 'L' => nil, 'T' => nil, 'R' => nil, 'B' => nil } + pdf.rounded_rect(50, 255, 40, 30, 6.50) + + content = [] + contents = pdf.getPageBuffer(page) + contents.each_line {|line| content.push line.chomp } + + assert_equal "160.16 119.06 m", content[before_size] # outPoint(x + rx, y) + assert_equal "236.69 119.06 l", content[before_size + 1] # outLine(xc, y) + assert_equal "246.87 119.06 255.12 110.81 255.12 100.63 c", content[before_size + 2] # outCurve(xc + (rx * myArc), yc - ry, xc + rx, yc - (ry * myArc), xc + rx, yc) + assert_equal "255.12 52.44 l", content[before_size + 3] # outLine(x + w, yc) + assert_equal "255.12 42.27 246.87 34.02 236.69 34.02 c", content[before_size + 4] # outCurve(xc + rx, yc + (ry * myArc), xc + (rx * myArc), yc + ry, xc, yc + ry) + assert_equal "160.16 34.02 l", content[before_size + 5] # outLine(xc, y + h) + assert_equal "149.98 34.02 141.73 42.27 141.73 52.44 c", content[before_size + 6] # outCurve(xc - (rx * myArc), yc + ry, xc - rx, yc + (ry * myArc), xc - rx, yc) + assert_equal "141.73 100.63 l", content[before_size + 7] # outLine(x, yc) + assert_equal "141.73 110.81 149.98 119.06 160.16 119.06 c", content[before_size + 8] # outCurve(xc - rx, yc - (ry * myArc), xc - (rx * myArc), yc - ry, xc, yc - ry) + assert_equal "S", content[before_size + 9] + end + + test "polygon basic test" do + pdf = MYPDF.new + pdf.add_page() + page = pdf.get_page + contents = pdf.getPageBuffer(page) + before_size = contents.split("\n").size + pdf.putshaders + style = { 'L' => nil, 'T' => nil, 'R' => nil, 'B' => nil } + pdf.polygon([5,135,45,135,15,165]) + + content = [] + contents = pdf.getPageBuffer(page) + contents.each_line {|line| content.push line.chomp } + + # draw = true + assert_equal "14.17 459.21 m", content[before_size] # outPoint(p[0], p[1]) + assert_equal "127.56 459.21 l", content[before_size + 1] # outLine(p[i], p[i + 1]) + assert_equal "42.52 374.17 l", content[before_size + 2] # outLine(p[i], p[i + 1]) + assert_equal "14.17 459.21 l", content[before_size + 3] # outLine(p[i], p[i + 1]) + assert_equal "127.56 459.21 l", content[before_size + 4] # outLine(p[i], p[i + 1]) + assert_equal "S", content[before_size + 5] + end + + test "polygon line_style test" do + pdf = MYPDF.new + pdf.add_page() + page = pdf.get_page + contents = pdf.getPageBuffer(page) + before_size = contents.split("\n").size + pdf.putshaders + style = { 'L' => nil, 'T' => nil, 'R' => nil, 'B' => nil } + style6 = {'width' => 0.5} + pdf.polygon([5,135,45,135,15,165], 'D', [style6, 0, 0]) + + content = [] + contents = pdf.getPageBuffer(page) + contents.each_line {|line| content.push line.chomp } + # draw = true + assert_equal "14.17 459.21 m", content[before_size] # outPoint(p[0], p[1]) + assert_equal "S", content[before_size + 1] + + assert_equal "1.42 w", content[before_size + 2] + assert_equal "14.17 459.21 m", content[before_size + 3] # outLine(p[i], p[i + 1]) + assert_equal "127.56 459.21 l", content[before_size + 4] # outLine(p[i], p[i + 1]) + assert_equal "S", content[before_size + 5] + assert_equal "127.56 459.21 m", content[before_size + 6] # outLine(p[i], p[i + 1]) + assert_equal "S", content[before_size + 7] + + assert_equal "1.42 w", content[before_size + 8] + assert_equal "14.17 459.21 m", content[before_size + 9] # outLine(p[i], p[i + 1]) + assert_equal "127.56 459.21 l", content[before_size + 10] # outLine(p[i], p[i + 1]) + assert_equal "S", content[before_size + 11] + + assert_equal "127.56 459.21 m", content[before_size + 12] # outLine(p[i], p[i + 1]) + assert_equal "S", content[before_size + 13] + end end From bda6fd9dff8f38eed58bf1db8b03df8e27d56a5c Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Thu, 28 Dec 2023 20:27:18 +0900 Subject: [PATCH 15/18] startSVGElementHandler: Improved handling of `use` tags in the absence of `defs` tags. --- lib/rbpdf.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index f40fb72..cfac359 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -18082,9 +18082,9 @@ def startSVGElementHandler(name, attribs, clipping = false) @svgclippaths[@svgclipid] << {'name' => name, 'attribs' => attribs} return end - if @svgdefsmode && !['clipPath', 'linearGradient', 'radialGradient', 'stop'].include?(name) + unless ['clipPath', 'linearGradient', 'radialGradient', 'stop'].include?(name) @svgdefs[attribs['id']] = {'name' => name, 'attribs' => attribs} - return + return if @svgdefsmode end prev_svgstyle = @svgstyles[@svgstyles.size - 1] # previous style @@ -18373,8 +18373,8 @@ def startSVGElementHandler(name, attribs, clipping = false) if attribs['xlink:href'] attribs.delete('xlink:href') end - if attribs['id'] - attribs.delete('id') + if use['id'] + use.delete('id') end attribs = use['attribs'].merge(attribs) startSVGElementHandler(use['name'], use['attribs']) From 8c0e17775e1daa16d356537feb4752d6d280b9bd Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Thu, 28 Dec 2023 21:30:05 +0900 Subject: [PATCH 16/18] Improved handling of `use` tags for `herf` attribute. See: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use --- lib/rbpdf.rb | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index cfac359..5463276 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -18133,9 +18133,10 @@ def startSVGElementHandler(name, attribs, clipping = false) @svggradients[@svggradientid]['gradientTransform'] = getSVGTransformMatrix(attribs['gradientTransform']) end @svggradients[@svggradientid]['coords'] = [x1, y1, x2, y2] - if attribs['xlink:href'] && !attribs['xlink:href'].empty? + if (attribs['href'] && !attribs['href'].empty?) || (attribs['xlink:href'] && !attribs['xlink:href'].empty?) + href = attribs['href'] ? 'href' : 'xlink:href' # gradient is defined on another place - @svggradients[@svggradientid]['xref'] = attribs['xlink:href'][1..-1] + @svggradients[@svggradientid]['xref'] = attribs[href][1..-1] end when 'radialGradient' @svggradientid = attribs['id'] @@ -18162,9 +18163,10 @@ def startSVGElementHandler(name, attribs, clipping = false) @svggradients[@svggradientid]['gradientTransform'] = getSVGTransformMatrix(attribs['gradientTransform']) end @svggradients[@svggradientid]['coords'] = [cx, cy, fx, fy, r] - if attribs['xlink:href'] && !attribs['xlink:href'].empty? + if (attribs['href'] && !attribs['href'].empty?) || (attribs['xlink:href'] && !attribs['xlink:href'].empty?) + href = attribs['href'] ? 'href' : 'xlink:href' # gradient is defined on another place - @svggradients[@svggradientid]['xref'] = attribs['xlink:href'][1..-1] + @svggradients[@svggradientid]['xref'] = attribs[href][1..-1] end when 'stop' # gradient stops @@ -18322,12 +18324,13 @@ def startSVGElementHandler(name, attribs, clipping = false) end end when 'image' # image - if attribs['xlink:href'] && !attribs['xlink:href'].empty? + if (attribs['href'] && !attribs['href'].empty?) || (attribs['xlink:href'] && !attribs['xlink:href'].empty?) + href = attribs['href'] ? 'href' : 'xlink:href' x = attribs['x'] ? getHTMLUnitToUnits(attribs['x'], 0, @svgunit, false) : 0 y = attribs['y'] ? getHTMLUnitToUnits(attribs['y'], 0, @svgunit, false) : 0 w = attribs['width'] ? getHTMLUnitToUnits(attribs['width'], 0, @svgunit, false) : 0 h = attribs['height'] ? getHTMLUnitToUnits(attribs['height'], 0, @svgunit, false) : 0 - img = attribs['xlink:href'] + img = attribs[href] unless clipping start_transform() svg_transform(tm) @@ -18368,15 +18371,14 @@ def startSVGElementHandler(name, attribs, clipping = false) set_xy(x, y, true) @svgstyles.push(svgstyle) when 'use' # use - if attribs['xlink:href'] - use = @svgdefs[attribs['xlink:href'][1..-1]] - if attribs['xlink:href'] - attribs.delete('xlink:href') - end - if use['id'] - use.delete('id') - end - attribs = use['attribs'].merge(attribs) + if attribs['href'] || attribs['xlink:href'] + href = attribs['href'] ? 'href' : 'xlink:href' + use = @svgdefs[attribs[href][1..-1]] + # Most attributes (except for x, y, width, height and (xlink:)href) do not override those set in the ancestor. + ['x', 'y', 'width', 'height', 'hred', 'xlink:href'].each do |attr| + use['attribs'].delete(attr) if attribs[attr] + end + attribs = attribs.merge(use['attribs']) startSVGElementHandler(use['name'], use['attribs']) end end From d2dc147906c27f57fe5de4364ef69472108da961 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Fri, 29 Dec 2023 21:44:08 +0900 Subject: [PATCH 17/18] endSVGElementHandler : fix defs mode problem. --- lib/rbpdf.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index 5463276..72dbe78 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -18435,10 +18435,14 @@ def endSVGElementHandler(name) when 'clipPath' # clipPath @svgclipmode = false when 'g' + return if @svgdefsmode + # ungroup: remove last style from array @svgstyles.pop stop_transform() when 'text', 'tspan' + return if @svgdefsmode + anchor = @svgstyles.last['text-anchor'] # print text cell(0, 0, @svgtext.strip, 0, 0, '', 0, '', 0, false, 'L', 'T', anchor) From 55b39b8364e6334f3076e6c48c96e9ba8c0fbdb2 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Fri, 29 Dec 2023 21:45:38 +0900 Subject: [PATCH 18/18] startSVGElementHandler : Fixed a problem with the path tag when the d attribute is missing. --- lib/rbpdf.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rbpdf.rb b/lib/rbpdf.rb index 72dbe78..14e4f2d 100755 --- a/lib/rbpdf.rb +++ b/lib/rbpdf.rb @@ -18182,7 +18182,7 @@ def startSVGElementHandler(name, attribs, clipping = false) opacity = svgstyle['stop-opacity'] ? svgstyle['stop-opacity'].to_f : 1 @svggradients[@svggradientid]['stops'] << {'offset' => offset, 'color' => stop_color, 'opacity' => opacity} when 'path' # paths - d = attribs['d'].strip + d = attribs['d'].to_s.strip if clipping svg_transform(tm) svg_path(d, 'CNZ')