test/hexapdf/layout/test_style.rb in hexapdf-0.5.0 vs test/hexapdf/layout/test_style.rb in hexapdf-0.6.0

- old
+ new

@@ -1,10 +1,13 @@ # -*- encoding: utf-8 -*- require 'test_helper' +require_relative '../content/common' +require 'hexapdf/document' require 'hexapdf/layout/style' -require 'hexapdf/layout/text_box' +require 'hexapdf/layout/text_layouter' +require 'hexapdf/layout/box' describe HexaPDF::Layout::Style::LineSpacing do before do @line1 = Object.new @line1.define_singleton_method(:y_min) { - 1} @@ -45,11 +48,11 @@ it "allows fixed line spacing" do obj = line_spacing(:fixed, 7) assert_equal(:fixed, obj.type) assert_equal(7, obj.value) assert_equal(7, obj.baseline_distance(@line1, @line2)) - assert_equal(7 - 1 - 4, obj.gap(@line1, @line2)) + assert_equal(7 - 1 - 4, obj.gap(@line1, @line2)) end it "allows line spacing using a leading value" do obj = line_spacing(:leading, 3) assert_equal(:leading, obj.type) @@ -70,68 +73,553 @@ it "raises an error if an invalid type is provided" do assert_raises(ArgumentError) { line_spacing(:invalid) } end end +describe HexaPDF::Layout::Style::Quad do + def create_quad(val) + HexaPDF::Layout::Style::Quad.new(val) + end + + describe "initialize" do + it "works with a single value" do + quad = create_quad(5) + assert_equal(5, quad.top) + assert_equal(5, quad.right) + assert_equal(5, quad.bottom) + assert_equal(5, quad.left) + end + + it "works with two values" do + quad = create_quad([5, 2]) + assert_equal(5, quad.top) + assert_equal(2, quad.right) + assert_equal(5, quad.bottom) + assert_equal(2, quad.left) + end + + it "works with three values" do + quad = create_quad([5, 2, 7]) + assert_equal(5, quad.top) + assert_equal(2, quad.right) + assert_equal(7, quad.bottom) + assert_equal(2, quad.left) + end + + it "works with four or more values" do + quad = create_quad([5, 2, 7, 1, 9]) + assert_equal(5, quad.top) + assert_equal(2, quad.right) + assert_equal(7, quad.bottom) + assert_equal(1, quad.left) + end + + it "works with a Quad as value" do + quad = create_quad([5, 2, 7, 1]) + new_quad = create_quad(quad) + assert_equal(new_quad.top, quad.top) + assert_equal(new_quad.right, quad.right) + assert_equal(new_quad.bottom, quad.bottom) + assert_equal(new_quad.left, quad.left) + end + end + + it "can be asked if it contains only a single value" do + assert(create_quad(5).simple?) + refute(create_quad([5, 2]).simple?) + end +end + +describe HexaPDF::Layout::Style::Border do + def create_border(*args) + HexaPDF::Layout::Style::Border.new(*args) + end + + it "has accessors for with, color and style that return Quads" do + border = create_border + assert_kind_of(HexaPDF::Layout::Style::Quad, border.width) + assert_kind_of(HexaPDF::Layout::Style::Quad, border.color) + assert_kind_of(HexaPDF::Layout::Style::Quad, border.style) + end + + it "can be asked whether a border is defined" do + assert(create_border.none?) + refute(create_border(width: 5).none?) + end + + describe "draw" do + before do + @canvas = HexaPDF::Document.new.pages.add.canvas + end + + describe "simple - same width, color and style on all sides" do + it "works with style solid" do + border = create_border(width: 10, color: 0.5, style: :solid) + border.draw(@canvas, 0, 0, 100, 100) + assert_operators(@canvas.contents, [[:save_graphics_state], + [:set_device_gray_stroking_color, [0.5]], + [:set_line_width, [10]], + [:append_rectangle, [5, 5, 90, 90]], + [:stroke_path], + [:restore_graphics_state]]) + end + + it "works with style dashed" do + border = create_border(width: 10, color: 0.5, style: :dashed) + border.draw(@canvas, 0, 0, 200, 300) + ops = [[:save_graphics_state], + [:set_device_gray_stroking_color, [0.5]], + [:set_line_width, [10]], + [:set_line_cap_style, [2]], + [:append_rectangle, [0, 0, 200, 300]], + [:clip_path_non_zero], [:end_path], + [:set_line_dash_pattern, [[10, 20], 25]], + [:move_to, [0, 295]], [:line_to, [200, 295]], + [:move_to, [200, 5]], [:line_to, [0, 5]], + [:stroke_path], + [:set_line_dash_pattern, [[10, 18], 23]], + [:move_to, [195, 300]], [:line_to, [195, 0]], + [:move_to, [5, 0]], [:line_to, [5, 300]], + [:stroke_path], + [:restore_graphics_state]] + assert_operators(@canvas.contents, ops) + end + + it "works with style dashed_round" do + border = create_border(width: 10, color: 0.5, style: :dashed_round) + border.draw(@canvas, 0, 0, 200, 300) + ops = [[:save_graphics_state], + [:set_device_gray_stroking_color, [0.5]], + [:set_line_width, [10]], + [:set_line_cap_style, [1]], + [:append_rectangle, [0, 0, 200, 300]], + [:clip_path_non_zero], [:end_path], + [:set_line_dash_pattern, [[10, 20], 25]], + [:move_to, [0, 295]], [:line_to, [200, 295]], + [:move_to, [200, 5]], [:line_to, [0, 5]], + [:stroke_path], + [:set_line_dash_pattern, [[10, 18], 23]], + [:move_to, [195, 300]], [:line_to, [195, 0]], + [:move_to, [5, 0]], [:line_to, [5, 300]], + [:stroke_path], + [:restore_graphics_state]] + assert_operators(@canvas.contents, ops) + end + + it "works with style dotted" do + border = create_border(width: 10, color: 0.5, style: :dotted) + border.draw(@canvas, 0, 0, 100, 200) + ops = [[:save_graphics_state], + [:set_device_gray_stroking_color, [0.5]], + [:set_line_width, [10]], + [:set_line_cap_style, [1]], + [:append_rectangle, [0, 0, 100, 200]], + [:clip_path_non_zero], [:end_path], + [:set_line_dash_pattern, [[0, 18], 13]], + [:move_to, [0, 195]], [:line_to, [100, 195]], + [:move_to, [100, 5]], [:line_to, [0, 5]], + [:stroke_path], + [:set_line_dash_pattern, [[0, 19], 14]], + [:move_to, [95, 200]], [:line_to, [95, 0]], + [:move_to, [5, 0]], [:line_to, [5, 200]], + [:stroke_path], + [:restore_graphics_state]] + assert_operators(@canvas.contents, ops) + end + end + + describe "complex borders where edges have different width/color/style values" do + it "works correctly for the top border" do + border = create_border(width: [10, 0, 0, 0], color: 0.5, style: :dashed) + border.draw(@canvas, 0, 0, 200, 300) + ops = [[:save_graphics_state], + [:save_graphics_state], + [:move_to, [0, 300]], [:line_to, [200, 300]], + [:line_to, [200, 290]], [:line_to, [0, 290]], + [:clip_path_non_zero], [:end_path], + [:set_device_gray_stroking_color, [0.5]], + [:set_line_width, [10]], + [:set_line_cap_style, [2]], + [:set_line_dash_pattern, [[10, 20], 25]], + [:move_to, [0, 295]], [:line_to, [200, 295]], + [:stroke_path], + [:restore_graphics_state], + [:restore_graphics_state]] + assert_operators(@canvas.contents, ops) + end + + it "works correctly for the right border" do + border = create_border(width: [0, 10, 0, 0], color: 0.5, style: :dashed) + border.draw(@canvas, 0, 0, 200, 300) + ops = [[:save_graphics_state], + [:save_graphics_state], + [:move_to, [200, 300]], [:line_to, [200, 0]], + [:line_to, [190, 0]], [:line_to, [190, 300]], + [:clip_path_non_zero], [:end_path], + [:set_device_gray_stroking_color, [0.5]], + [:set_line_width, [10]], + [:set_line_cap_style, [2]], + [:set_line_dash_pattern, [[10, 18], 23]], + [:move_to, [195, 300]], [:line_to, [195, 0]], + [:stroke_path], + [:restore_graphics_state], + [:restore_graphics_state]] + assert_operators(@canvas.contents, ops) + end + + it "works correctly for the bottom border" do + border = create_border(width: [0, 0, 10, 0], color: 0.5, style: :dashed) + border.draw(@canvas, 0, 0, 200, 300) + ops = [[:save_graphics_state], + [:save_graphics_state], + [:move_to, [200, 0]], [:line_to, [0, 0]], + [:line_to, [0, 10]], [:line_to, [200, 10]], + [:clip_path_non_zero], [:end_path], + [:set_device_gray_stroking_color, [0.5]], + [:set_line_width, [10]], + [:set_line_cap_style, [2]], + [:set_line_dash_pattern, [[10, 20], 25]], + [:move_to, [200, 5]], [:line_to, [0, 5]], + [:stroke_path], + [:restore_graphics_state], + [:restore_graphics_state]] + assert_operators(@canvas.contents, ops) + end + + it "works correctly for the left border" do + border = create_border(width: [0, 0, 0, 10], color: 0.5, style: :dashed) + border.draw(@canvas, 0, 0, 200, 300) + ops = [[:save_graphics_state], + [:save_graphics_state], + [:move_to, [0, 0]], [:line_to, [0, 300]], + [:line_to, [10, 300]], [:line_to, [10, 0]], + [:clip_path_non_zero], [:end_path], + [:set_device_gray_stroking_color, [0.5]], + [:set_line_width, [10]], + [:set_line_cap_style, [2]], + [:set_line_dash_pattern, [[10, 18], 23]], + [:move_to, [5, 0]], [:line_to, [5, 300]], + [:stroke_path], + [:restore_graphics_state], + [:restore_graphics_state]] + assert_operators(@canvas.contents, ops) + end + + it "works with all values combined" do + border = create_border(width: [20, 10, 40, 30], color: [0, 0.25, 0.5, 0.75], + style: [:solid, :dashed, :dashed_round, :dotted]) + border.draw(@canvas, 0, 0, 100, 200) + ops = [[:save_graphics_state], + [:save_graphics_state], + [:move_to, [0, 200]], [:line_to, [100, 200]], + [:line_to, [90, 180]], [:line_to, [30, 180]], [:clip_path_non_zero], [:end_path], + [:set_line_width, [20]], + [:move_to, [0, 190]], [:line_to, [100, 190]], [:stroke_path], + [:restore_graphics_state], [:save_graphics_state], + [:move_to, [100, 200]], [:line_to, [100, 0]], + [:line_to, [90, 40]], [:line_to, [90, 180]], [:clip_path_non_zero], [:end_path], + [:set_device_gray_stroking_color, [0.25]], [:set_line_width, [10]], + [:set_line_cap_style, [2]], [:set_line_dash_pattern, [[10, 20], 25]], + [:move_to, [95, 200]], [:line_to, [95, 0]], [:stroke_path], + [:restore_graphics_state], [:save_graphics_state], + [:move_to, [100, 0]], [:line_to, [0, 0]], + [:line_to, [30, 40]], [:line_to, [90, 40]], [:clip_path_non_zero], [:end_path], + [:set_device_gray_stroking_color, [0.5]], [:set_line_width, [40]], + [:set_line_cap_style, [1]], [:set_line_dash_pattern, [[40, 0], 20]], + [:move_to, [100, 20]], [:line_to, [0, 20]], [:stroke_path], + [:restore_graphics_state], [:save_graphics_state], + [:move_to, [0, 0]], [:line_to, [0, 200]], + [:line_to, [30, 180]], [:line_to, [30, 40]], [:clip_path_non_zero], [:end_path], + [:set_device_gray_stroking_color, [0.75]], [:set_line_width, [30]], + [:set_line_cap_style, [1]], [:set_line_dash_pattern, [[0, 42.5], 27.5]], + [:move_to, [15, 0]], [:line_to, [15, 200]], [:stroke_path], + [:restore_graphics_state], [:restore_graphics_state]] + assert_operators(@canvas.contents, ops) + end + end + + it "raises an error if an invalid style is provided" do + assert_raises(ArgumentError) do + create_border(width: 1, color: 0, style: :unknown).draw(@canvas, 0, 0, 10, 10) + end + end + end +end + +describe HexaPDF::Layout::Style::Layers do + before do + @layers = HexaPDF::Layout::Style::Layers.new + end + + it "can be initialized with an array of layers" do + data = [-> {}] + layers = HexaPDF::Layout::Style::Layers.new(data) + assert_equal(data, layers.enum_for(:each, {}).to_a) + end + + describe "add and each" do + it "can use a given block" do + block = proc { true} + @layers.add(&block) + assert_equal([block], @layers.enum_for(:each, {}).to_a) + end + + it "can store a reference" do + @layers.add(:link, option: :value) + value = Object.new + value.define_singleton_method(:new) {|*args| :new } + config = Object.new + config.define_singleton_method(:constantize) {|*args| value } + assert_equal([:new], @layers.enum_for(:each, config).to_a) + end + + it "fails if neither a block nor a name is given when adding a layer" do + assert_raises(ArgumentError) { @layers.add } + end + end + + it "can determine whether layers are defined" do + assert(@layers.none?) + @layers.add {} + refute(@layers.none?) + end + + it "draws the layers onto a canvas" do + box = Object.new + value = nil + klass = Class.new + klass.send(:define_method, :initialize) {|**args| @args = args } + klass.send(:define_method, :call) do |canvas, _| + value = @args + canvas.line_width(5) + end + canvas = HexaPDF::Document.new.pages.add.canvas + canvas.context.document.config['style.layers_map'][:test] = klass + + @layers.add {|canv, ibox| assert_equal(box, ibox); canv.line_width(10)} + @layers.add(:test, option: :value) + @layers.draw(canvas, 10, 15, box) + ops = [[:save_graphics_state], + [:concatenate_matrix, [1, 0, 0, 1, 10, 15]], + [:save_graphics_state], + [:set_line_width, [10]], + [:restore_graphics_state], + [:save_graphics_state], + [:set_line_width, [5]], + [:restore_graphics_state], + [:restore_graphics_state]] + assert_operators(canvas.contents, ops) + assert_equal({option: :value}, value) + end +end + +describe HexaPDF::Layout::Style::LinkLayer do + describe "initialize" do + it "fails if more than one possible target is chosen" do + assert_raises(ArgumentError) { HexaPDF::Layout::Style::LinkLayer.new(dest: true, uri: true) } + assert_raises(ArgumentError) { HexaPDF::Layout::Style::LinkLayer.new(dest: true, file: true) } + assert_raises(ArgumentError) { HexaPDF::Layout::Style::LinkLayer.new(uri: true, file: true) } + end + + it "fails if an invalid border is provided" do + assert_raises(ArgumentError) { HexaPDF::Layout::Style::LinkLayer.new(border: 5) } + end + end + + describe "call" do + before do + @canvas = HexaPDF::Document.new.pages.add.canvas + @canvas.translate(10, 10) + @box = HexaPDF::Layout::Box.new(width: 15, height: 10) + end + + def call_link(hash) + link = HexaPDF::Layout::Style::LinkLayer.new(hash) + link.call(@canvas, @box) + @canvas.context[:Annots][0] + end + + it "sets general values like /Rect and /QuadPoints" do + annot = call_link(dest: true) + assert_equal(:Link, annot[:Subtype]) + assert_equal([10, 10, 25, 20], annot[:Rect].value) + assert_equal([10, 10, 25, 10, 25, 20, 10, 20], annot[:QuadPoints]) + end + + it "removes the border by default" do + annot = call_link(dest: true) + assert_equal([0, 0, 0], annot[:Border]) + end + + it "uses a default border if no specific border style is specified" do + annot = call_link(dest: true, border: true) + assert_equal([0, 0, 1], annot[:Border]) + end + + it "uses the specified border and border color" do + annot = call_link(dest: true, border: [10, 10, 2], border_color: [255]) + assert_equal([10, 10, 2], annot[:Border]) + assert_equal([1.0], annot[:C]) + end + + it "works for simple destinations" do + annot = call_link(dest: [@canvas.context, :FitH]) + assert_equal([@canvas.context, :FitH], annot[:Dest]) + assert_nil(annot[:A]) + end + + it "works for URIs" do + annot = call_link(uri: "test.html") + assert_equal({S: :URI, URI: "test.html"}, annot[:A].value) + assert_nil(annot[:Dest]) + end + + it "works for files" do + annot = call_link(file: "local-file.pdf") + assert_equal({S: :Launch, F: "local-file.pdf", NewWindow: true}, annot[:A].value) + assert_nil(annot[:Dest]) + end + end +end + describe HexaPDF::Layout::Style do before do @style = HexaPDF::Layout::Style.new end it "can assign values on initialization" do style = HexaPDF::Layout::Style.new(font_size: 10) assert_equal(10, style.font_size) end - it "has several dynamically generated properties with default values" do + it "has several simple and dynamically generated properties with default values" do assert_raises(HexaPDF::Error) { @style.font } assert_equal(10, @style.font_size) assert_equal(0, @style.character_spacing) assert_equal(0, @style.word_spacing) assert_equal(100, @style.horizontal_scaling) assert_equal(0, @style.text_rise) assert_equal({}, @style.font_features) + assert_equal(:fill, @style.text_rendering_mode) + assert_equal([0], @style.fill_color.components) + assert_equal(1, @style.fill_alpha) + assert_equal([0], @style.stroke_color.components) + assert_equal(1, @style.stroke_alpha) + assert_equal(1, @style.stroke_width) + assert_equal(:butt, @style.stroke_cap_style) + assert_equal(:miter, @style.stroke_join_style) + assert_equal(10.0, @style.stroke_miter_limit) assert_equal(:left, @style.align) assert_equal(:top, @style.valign) + assert_equal(0, @style.text_indent) + assert_nil(@style.background_color) + assert(@style.padding.simple?) + assert_equal(0, @style.padding.top) + assert(@style.margin.simple?) + assert_equal(0, @style.margin.top) + assert(@style.border.none?) + assert_equal([[], 0], @style.stroke_dash_pattern.to_operands) + assert_equal([:proportional, 1], [@style.line_spacing.type, @style.line_spacing.value]) + refute(@style.subscript) + refute(@style.superscript) + assert_kind_of(HexaPDF::Layout::Style::Layers, @style.underlays) + assert_kind_of(HexaPDF::Layout::Style::Layers, @style.overlays) end - it "can set and retrieve line spacing objects" do - assert_equal([:proportional, 1], [@style.line_spacing.type, @style.line_spacing.value]) - @style.line_spacing = :double - assert_equal([:proportional, 2], [@style.line_spacing.type, @style.line_spacing.value]) + it "allows using a non-standard setter for generated properties" do + @style.padding = [5, 3] + assert_equal(5, @style.padding.top) + assert_equal(3, @style.padding.left) + + @style.stroke_dash_pattern(5, 2) + assert_equal([[5], 2], @style.stroke_dash_pattern.to_operands) end - it "can set and retrieve text segmentation algorithms" do - assert_equal(HexaPDF::Layout::TextBox::SimpleTextSegmentation, + it "allows checking whether a property has been set or accessed" do + refute(@style.align?) + assert_equal(:left, @style.align) + assert(@style.align?) + + refute(@style.valign?) + @style.valign = :bottom + assert(@style.valign?) + end + + it "has several dynamically generated properties with default values that take blocks" do + assert_equal(HexaPDF::Layout::TextLayouter::SimpleTextSegmentation, @style.text_segmentation_algorithm) + assert_equal(HexaPDF::Layout::TextLayouter::SimpleLineWrapping, + @style.text_line_wrapping_algorithm) + block = proc { :y } @style.text_segmentation_algorithm(&block) assert_equal(block, @style.text_segmentation_algorithm) - end - it "can set and retrieve line wrapping algorithms" do - assert_equal(HexaPDF::Layout::TextBox::SimpleLineWrapping, - @style.text_line_wrapping_algorithm) - @style.text_line_wrapping_algorithm(:callable) - assert_equal(:callable, @style.text_line_wrapping_algorithm) + @style.text_segmentation_algorithm(:callable) + assert_equal(:callable, @style.text_segmentation_algorithm) end - it "has methods for some derived and cached values" do - assert_equal(0.01, @style.scaled_font_size) - assert_equal(0, @style.scaled_character_spacing) - assert_equal(0, @style.scaled_word_spacing) - assert_equal(1, @style.scaled_horizontal_scaling) + describe "methods for some derived and cached values" do + before do + wrapped_font = Object.new + wrapped_font.define_singleton_method(:ascender) { 600 } + wrapped_font.define_singleton_method(:descender) { -100 } + font = Object.new + font.define_singleton_method(:scaling_factor) { 1 } + font.define_singleton_method(:wrapped_font) { wrapped_font } + @style.font = font + end - wrapped_font = Object.new - wrapped_font.define_singleton_method(:ascender) { 600 } - wrapped_font.define_singleton_method(:descender) { -100 } - font = Object.new - font.define_singleton_method(:scaling_factor) { 1 } - font.define_singleton_method(:wrapped_font) { wrapped_font } - @style.font = font + it "computes them correctly" do + @style.horizontal_scaling(200).character_spacing(1).word_spacing(2) + assert_equal(0.02, @style.scaled_font_size) + assert_equal(2, @style.scaled_character_spacing) + assert_equal(4, @style.scaled_word_spacing) + assert_equal(2, @style.scaled_horizontal_scaling) - assert_equal(6, @style.scaled_font_ascender) - assert_equal(-1, @style.scaled_font_descender) + assert_equal(6, @style.scaled_font_ascender) + assert_equal(-1, @style.scaled_font_descender) + end + + it "computes item widths correctly" do + @style.horizontal_scaling(200).character_spacing(1).word_spacing(2) + + assert_equal(-1.0, @style.scaled_item_width(50)) + + obj = Object.new + obj.define_singleton_method(:width) { 100 } + obj.define_singleton_method(:apply_word_spacing?) { true } + assert_equal(8, @style.scaled_item_width(obj)) + end + + it "handles subscript" do + @style.subscript = true + assert_in_delta(5.83, @style.calculated_font_size) + assert_in_delta(0.00583, @style.scaled_font_size, 0.000001) + assert_in_delta(-2.00, @style.calculated_text_rise) + end + + it "handles superscript" do + @style.superscript = true + assert_in_delta(5.83, @style.calculated_font_size) + assert_in_delta(3.30, @style.calculated_text_rise) + end + + it "handles underline" do + @style.font.wrapped_font.define_singleton_method(:underline_position) { -100 } + @style.font.wrapped_font.define_singleton_method(:underline_thickness) { 10 } + @style.text_rise = 10 + assert_in_delta(-1.05 + 10, @style.calculated_underline_position) + assert_equal(0.1, @style.calculated_underline_thickness) + end + + it "handles strikeout" do + @style.font.wrapped_font.define_singleton_method(:strikeout_position) { 300 } + @style.font.wrapped_font.define_singleton_method(:strikeout_thickness) { 10 } + @style.text_rise = 10 + assert_in_delta(2.95 + 10, @style.calculated_strikeout_position) + assert_equal(0.1, @style.calculated_strikeout_thickness) + end end it "can clear cached values" do assert_equal(0.01, @style.scaled_font_size) @style.font_size = 20