hexapdf 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +79 -0
- data/README.md +1 -1
- data/lib/hexapdf/cli/command.rb +63 -63
- data/lib/hexapdf/cli/inspect.rb +14 -5
- data/lib/hexapdf/cli/modify.rb +0 -1
- data/lib/hexapdf/cli/optimize.rb +5 -5
- data/lib/hexapdf/composer.rb +14 -0
- data/lib/hexapdf/configuration.rb +26 -0
- data/lib/hexapdf/content/graphics_state.rb +1 -1
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +1 -1
- data/lib/hexapdf/document/annotations.rb +173 -0
- data/lib/hexapdf/document/layout.rb +45 -6
- data/lib/hexapdf/document.rb +28 -7
- data/lib/hexapdf/error.rb +11 -3
- data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
- data/lib/hexapdf/font/true_type_wrapper.rb +1 -0
- data/lib/hexapdf/font/type1_wrapper.rb +1 -0
- data/lib/hexapdf/layout/style.rb +101 -7
- data/lib/hexapdf/object.rb +2 -2
- data/lib/hexapdf/pdf_array.rb +25 -3
- data/lib/hexapdf/tokenizer.rb +4 -1
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
- data/lib/hexapdf/type/acro_form/field.rb +1 -0
- data/lib/hexapdf/type/acro_form/form.rb +7 -6
- data/lib/hexapdf/type/acro_form/java_script_actions.rb +9 -2
- data/lib/hexapdf/type/acro_form/text_field.rb +9 -2
- data/lib/hexapdf/type/annotation.rb +71 -1
- data/lib/hexapdf/type/annotations/appearance_generator.rb +348 -0
- data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
- data/lib/hexapdf/type/annotations/border_styling.rb +160 -0
- data/lib/hexapdf/type/annotations/circle.rb +65 -0
- data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
- data/lib/hexapdf/type/annotations/line.rb +490 -0
- data/lib/hexapdf/type/annotations/square.rb +65 -0
- data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
- data/lib/hexapdf/type/annotations/widget.rb +52 -116
- data/lib/hexapdf/type/annotations.rb +8 -0
- data/lib/hexapdf/type/form.rb +2 -2
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +0 -1
- data/lib/hexapdf/xref_section.rb +7 -4
- data/test/hexapdf/content/test_graphics_state.rb +2 -3
- data/test/hexapdf/content/test_operator.rb +4 -5
- data/test/hexapdf/digital_signature/test_cms_handler.rb +7 -8
- data/test/hexapdf/digital_signature/test_handler.rb +2 -3
- data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +1 -2
- data/test/hexapdf/document/test_annotations.rb +55 -0
- data/test/hexapdf/document/test_layout.rb +24 -2
- data/test/hexapdf/font/test_true_type_wrapper.rb +7 -0
- data/test/hexapdf/font/test_type1_wrapper.rb +7 -0
- data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
- data/test/hexapdf/layout/test_style.rb +27 -2
- data/test/hexapdf/task/test_optimize.rb +1 -1
- data/test/hexapdf/test_composer.rb +7 -0
- data/test/hexapdf/test_document.rb +11 -3
- data/test/hexapdf/test_object.rb +1 -1
- data/test/hexapdf/test_pdf_array.rb +36 -3
- data/test/hexapdf/test_stream.rb +1 -2
- data/test/hexapdf/test_xref_section.rb +1 -1
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +78 -3
- data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
- data/test/hexapdf/type/acro_form/test_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_form.rb +17 -1
- data/test/hexapdf/type/acro_form/test_java_script_actions.rb +21 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +7 -1
- data/test/hexapdf/type/annotations/test_appearance_generator.rb +482 -0
- data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
- data/test/hexapdf/type/annotations/test_border_styling.rb +114 -0
- data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
- data/test/hexapdf/type/annotations/test_line.rb +169 -0
- data/test/hexapdf/type/annotations/test_widget.rb +35 -81
- data/test/hexapdf/type/test_annotation.rb +55 -0
- data/test/hexapdf/type/test_form.rb +6 -0
- metadata +17 -2
| @@ -174,11 +174,59 @@ module HexaPDF | |
| 174 174 |  | 
| 175 175 | 
             
                    alias create_radio_button_appearances create_check_box_appearances
         | 
| 176 176 |  | 
| 177 | 
            -
                    # Creates the appropriate appearances for push  | 
| 177 | 
            +
                    # Creates the appropriate appearances for push button fields
         | 
| 178 178 | 
             
                    #
         | 
| 179 | 
            -
                    #  | 
| 179 | 
            +
                    # The following describes how the appearance is built:
         | 
| 180 | 
            +
                    #
         | 
| 181 | 
            +
                    # * The widget's rectangle /Rect must be defined.
         | 
| 182 | 
            +
                    #
         | 
| 183 | 
            +
                    # * If the font size (used for the caption) is zero, a font size of
         | 
| 184 | 
            +
                    #   +acro_form.default_font_size+ is used.
         | 
| 185 | 
            +
                    #
         | 
| 186 | 
            +
                    # * The line width, style and color of the rectangle are taken from the widget's border
         | 
| 187 | 
            +
                    #   style. See HexaPDF::Type::Annotations::Widget#border_style.
         | 
| 188 | 
            +
                    #
         | 
| 189 | 
            +
                    # * The background color is determined by the widget's background color. See
         | 
| 190 | 
            +
                    #   HexaPDF::Type::Annotations::Widget#background_color.
         | 
| 180 191 | 
             
                    def create_push_button_appearances
         | 
| 181 | 
            -
                       | 
| 192 | 
            +
                      default_resources = @document.acro_form(create: true).default_resources
         | 
| 193 | 
            +
                      border_style = @widget.border_style
         | 
| 194 | 
            +
                      padding = border_style.width
         | 
| 195 | 
            +
                      marker_style = @widget.marker_style
         | 
| 196 | 
            +
                      font = retrieve_font_information(marker_style.font_name, default_resources)
         | 
| 197 | 
            +
             | 
| 198 | 
            +
                      @widget[:AS] = :N
         | 
| 199 | 
            +
                      @widget.flag(:print)
         | 
| 200 | 
            +
                      @widget.unflag(:hidden)
         | 
| 201 | 
            +
                      rect = @widget[:Rect]
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                      width, height, matrix = perform_rotation(rect.width, rect.height)
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                      form = (@widget[:AP] ||= {})[:N] ||= @document.add({Type: :XObject, Subtype: :Form})
         | 
| 206 | 
            +
                      # Wrap existing object in Form class in case the PDF writer didn't include the /Subtype
         | 
| 207 | 
            +
                      # key or the type of the object is wrong; we can do this since we know this has to be a
         | 
| 208 | 
            +
                      # Form object
         | 
| 209 | 
            +
                      unless form.type == :XObject && form[:Subtype] == :Form
         | 
| 210 | 
            +
                        form = @document.wrap(form, type: :XObject, subtype: :Form)
         | 
| 211 | 
            +
                      end
         | 
| 212 | 
            +
                      form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height],
         | 
| 213 | 
            +
                                          Matrix: matrix, Resources: HexaPDF::Object.deep_copy(default_resources)})
         | 
| 214 | 
            +
                      form.contents = ''
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                      canvas = form.canvas
         | 
| 217 | 
            +
                      apply_background_and_border(border_style, canvas)
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                      style = HexaPDF::Layout::Style.new(font: font, font_size: marker_style.size,
         | 
| 220 | 
            +
                                                         fill_color: marker_style.color)
         | 
| 221 | 
            +
                      if (text = marker_style.style) && text.kind_of?(String)
         | 
| 222 | 
            +
                        items = @document.layout.text_fragments(marker_style.style, style: style)
         | 
| 223 | 
            +
                        layouter = Layout::TextLayouter.new(style)
         | 
| 224 | 
            +
                        layouter.style.text_align(:center).text_valign(:center).line_spacing(:proportional, 1.25)
         | 
| 225 | 
            +
                        result = layouter.fit(items, width - 2 * padding, height - 2 * padding)
         | 
| 226 | 
            +
                        unless result.lines.empty?
         | 
| 227 | 
            +
                          result.draw(canvas, padding, height - padding)
         | 
| 228 | 
            +
                        end
         | 
| 229 | 
            +
                      end
         | 
| 182 230 | 
             
                    end
         | 
| 183 231 |  | 
| 184 232 | 
             
                    # Creates the appropriate appearances for text fields, combo box fields and list box fields.
         | 
| @@ -207,7 +255,8 @@ module HexaPDF | |
| 207 255 | 
             
                    # Note: Rich text fields are currently not supported!
         | 
| 208 256 | 
             
                    def create_text_appearances
         | 
| 209 257 | 
             
                      default_resources = @document.acro_form.default_resources
         | 
| 210 | 
            -
                       | 
| 258 | 
            +
                      font_name, font_size, font_color = @field.parse_default_appearance_string(@widget)
         | 
| 259 | 
            +
                      font = retrieve_font_information(font_name, default_resources)
         | 
| 211 260 | 
             
                      style = HexaPDF::Layout::Style.new(font: font, font_size: font_size, fill_color: font_color)
         | 
| 212 261 | 
             
                      border_style = @widget.border_style
         | 
| 213 262 | 
             
                      padding = [1, border_style.width].max
         | 
| @@ -475,9 +524,9 @@ module HexaPDF | |
| 475 524 | 
             
                      end
         | 
| 476 525 | 
             
                    end
         | 
| 477 526 |  | 
| 478 | 
            -
                    # Returns the font wrapper and font  | 
| 479 | 
            -
                     | 
| 480 | 
            -
             | 
| 527 | 
            +
                    # Returns the font wrapper, font size and font color to be used for variable text fields and
         | 
| 528 | 
            +
                    # push button captions.
         | 
| 529 | 
            +
                    def retrieve_font_information(font_name, resources)
         | 
| 481 530 | 
             
                      font_object = resources.font(font_name) rescue nil
         | 
| 482 531 | 
             
                      font = font_object&.font_wrapper
         | 
| 483 532 | 
             
                      unless font
         | 
| @@ -493,7 +542,7 @@ module HexaPDF | |
| 493 542 | 
             
                          raise(HexaPDF::Error, "Font #{font_name} of the AcroForm's default resources not usable")
         | 
| 494 543 | 
             
                        end
         | 
| 495 544 | 
             
                      end
         | 
| 496 | 
            -
                       | 
| 545 | 
            +
                      font
         | 
| 497 546 | 
             
                    end
         | 
| 498 547 |  | 
| 499 548 | 
             
                    # Calculates the font size for single line text fields using auto-sizing, based on the font
         | 
| @@ -568,12 +568,12 @@ module HexaPDF | |
| 568 568 | 
             
                      seen = {} # used for combining field
         | 
| 569 569 |  | 
| 570 570 | 
             
                      validate_array = lambda do |parent, container|
         | 
| 571 | 
            -
                        container. | 
| 571 | 
            +
                        container.map! do |field|
         | 
| 572 572 | 
             
                          if !field.kind_of?(HexaPDF::Object) || !field.kind_of?(HexaPDF::Dictionary) || field.null?
         | 
| 573 573 | 
             
                            yield("Invalid object in AcroForm field hierarchy", true)
         | 
| 574 | 
            -
                            next  | 
| 574 | 
            +
                            next nil
         | 
| 575 575 | 
             
                          end
         | 
| 576 | 
            -
                          next  | 
| 576 | 
            +
                          next field unless field.key?(:T) # Skip widgets
         | 
| 577 577 |  | 
| 578 578 | 
             
                          field = Field.wrap(document, field)
         | 
| 579 579 | 
             
                          reject = false
         | 
| @@ -597,14 +597,15 @@ module HexaPDF | |
| 597 597 | 
             
                              widget[:Parent] = other_field
         | 
| 598 598 | 
             
                              kids << widget
         | 
| 599 599 | 
             
                            end
         | 
| 600 | 
            +
                            document.delete(field)
         | 
| 600 601 | 
             
                            reject = true
         | 
| 601 602 | 
             
                          elsif !reject
         | 
| 602 603 | 
             
                            seen[name] = field
         | 
| 603 604 | 
             
                          end
         | 
| 604 605 |  | 
| 605 | 
            -
                          validate_array.call(field, field[:Kids]) if field.key?(:Kids)
         | 
| 606 | 
            -
                          reject
         | 
| 607 | 
            -
                        end
         | 
| 606 | 
            +
                          validate_array.call(field, field[:Kids]) if !field.null? && field.key?(:Kids)
         | 
| 607 | 
            +
                          reject ? nil : field
         | 
| 608 | 
            +
                        end.compact!
         | 
| 608 609 | 
             
                      end
         | 
| 609 610 | 
             
                      validate_array.call(nil, root_fields)
         | 
| 610 611 |  | 
| @@ -496,7 +496,7 @@ module HexaPDF | |
| 496 496 | 
             
                               else
         | 
| 497 497 | 
             
                                 nil
         | 
| 498 498 | 
             
                               end
         | 
| 499 | 
            -
                      result && (result == result.truncate ? result.to_i.to_s : result.to_s)
         | 
| 499 | 
            +
                      result && (result.finite? && result == result.truncate ? result.to_i.to_s : result.to_s)
         | 
| 500 500 | 
             
                    end
         | 
| 501 501 |  | 
| 502 502 | 
             
                    AF_SIMPLE_CALCULATE_MAPPING = { #:nodoc:
         | 
| @@ -613,7 +613,14 @@ module HexaPDF | |
| 613 613 |  | 
| 614 614 | 
             
                    # Returns the numeric value of the string, interpreting comma as point.
         | 
| 615 615 | 
             
                    def af_make_number(value)
         | 
| 616 | 
            -
                      value.to_s | 
| 616 | 
            +
                      value = value.to_s
         | 
| 617 | 
            +
                      if value.match?(/(?:[+-])?Inf(?:inity)?/i)
         | 
| 618 | 
            +
                        value.start_with?('-') ? -Float::INFINITY : Float::INFINITY
         | 
| 619 | 
            +
                      elsif value.match?(/NaN/i)
         | 
| 620 | 
            +
                        Float::NAN
         | 
| 621 | 
            +
                      else
         | 
| 622 | 
            +
                        value.tr(',', '.').to_f
         | 
| 623 | 
            +
                      end
         | 
| 617 624 | 
             
                    end
         | 
| 618 625 |  | 
| 619 626 | 
             
                    # Formats the numeric value according to the format string and separator style.
         | 
| @@ -176,7 +176,7 @@ module HexaPDF | |
| 176 176 | 
             
                      end
         | 
| 177 177 | 
             
                      str = str.gsub(/[[:space:]]/, ' ') if str && concrete_field_type == :single_line_text_field
         | 
| 178 178 | 
             
                      if key?(:MaxLen) && str && str.length > self[:MaxLen]
         | 
| 179 | 
            -
                         | 
| 179 | 
            +
                        str = @document.config['acro_form.text_field.on_max_len_exceeded'].call(self, str)
         | 
| 180 180 | 
             
                      end
         | 
| 181 181 | 
             
                      self[:V] = str
         | 
| 182 182 | 
             
                      update_widgets
         | 
| @@ -348,7 +348,14 @@ module HexaPDF | |
| 348 348 | 
             
                        return
         | 
| 349 349 | 
             
                      end
         | 
| 350 350 | 
             
                      if (max_len = self[:MaxLen]) && field_value && field_value.length > max_len
         | 
| 351 | 
            -
                         | 
| 351 | 
            +
                        correctable = true
         | 
| 352 | 
            +
                        begin
         | 
| 353 | 
            +
                          str = @document.config['acro_form.text_field.on_max_len_exceeded'].call(self, field_value)
         | 
| 354 | 
            +
                        rescue HexaPDF::Error
         | 
| 355 | 
            +
                          correctable = false
         | 
| 356 | 
            +
                        end
         | 
| 357 | 
            +
                        yield("Text contents of field '#{full_field_name}' is too long", correctable)
         | 
| 358 | 
            +
                        self.field_value = str if correctable
         | 
| 352 359 | 
             
                      end
         | 
| 353 360 | 
             
                      if comb_text_field? && !max_len
         | 
| 354 361 | 
             
                        yield("Comb text field needs a value for /MaxLen")
         | 
| @@ -113,6 +113,18 @@ module HexaPDF | |
| 113 113 |  | 
| 114 114 | 
             
                  end
         | 
| 115 115 |  | 
| 116 | 
            +
                  # Border effect dictionary used by square, circle and polygon annotation types.
         | 
| 117 | 
            +
                  #
         | 
| 118 | 
            +
                  # See: PDF2.0 s12.5.4
         | 
| 119 | 
            +
                  class BorderEffect < Dictionary
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                    define_type :XXBorderEffect
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                    define_field :S,    type: Symbol, default: :S, allowed_values: [:C, :S]
         | 
| 124 | 
            +
                    define_field :I,    type: Numeric, default: 0, allowed_values: [0, 1, 2]
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  end
         | 
| 127 | 
            +
             | 
| 116 128 | 
             
                  extend Utils::BitField
         | 
| 117 129 |  | 
| 118 130 | 
             
                  define_type :Annot
         | 
| @@ -133,7 +145,7 @@ module HexaPDF | |
| 133 145 | 
             
                  define_field :OC,           type: Dictionary, version: '1.5'
         | 
| 134 146 | 
             
                  define_field :AF,           type: PDFArray, version: '2.0'
         | 
| 135 147 | 
             
                  define_field :ca,           type: Numeric, default: 1.0, version: '2.0'
         | 
| 136 | 
            -
                  define_field :CA,           type: Numeric, default: 1.0, version: ' | 
| 148 | 
            +
                  define_field :CA,           type: Numeric, default: 1.0, version: '1.4'
         | 
| 137 149 | 
             
                  define_field :BM,           type: Symbol, version: '2.0'
         | 
| 138 150 | 
             
                  define_field :Lang,         type: String, version: '2.0'
         | 
| 139 151 |  | 
| @@ -259,6 +271,64 @@ module HexaPDF | |
| 259 271 | 
             
                    xobject
         | 
| 260 272 | 
             
                  end
         | 
| 261 273 |  | 
| 274 | 
            +
                  # Regenerates the appearance stream of the annotation.
         | 
| 275 | 
            +
                  #
         | 
| 276 | 
            +
                  # This uses the information stored in the annotation to regenerate the appearance.
         | 
| 277 | 
            +
                  #
         | 
| 278 | 
            +
                  # See: Annotations::AppearanceGenerator
         | 
| 279 | 
            +
                  def regenerate_appearance
         | 
| 280 | 
            +
                    appearance_generator_class = document.config.constantize('annotation.appearance_generator')
         | 
| 281 | 
            +
                    appearance_generator_class.new(self).create_appearance
         | 
| 282 | 
            +
                  end
         | 
| 283 | 
            +
             | 
| 284 | 
            +
                  # :call-seq:
         | 
| 285 | 
            +
                  #   annot.contents        => contents or +nil+
         | 
| 286 | 
            +
                  #   annot.contents(text)  => annot
         | 
| 287 | 
            +
                  #
         | 
| 288 | 
            +
                  # Returns the text of the annotation when no argument is given. Otherwise sets the text and
         | 
| 289 | 
            +
                  # returns self.
         | 
| 290 | 
            +
                  #
         | 
| 291 | 
            +
                  # The contents is used differently depending on the annotation type. It is either the text
         | 
| 292 | 
            +
                  # that should be displayed for the annotation or an alternate description of the annotation's
         | 
| 293 | 
            +
                  # contents.
         | 
| 294 | 
            +
                  #
         | 
| 295 | 
            +
                  # A value of +nil+ means deleting the existing contents entry.
         | 
| 296 | 
            +
                  def contents(text = :UNSET)
         | 
| 297 | 
            +
                    if text == :UNSET
         | 
| 298 | 
            +
                      self[:Contents]
         | 
| 299 | 
            +
                    else
         | 
| 300 | 
            +
                      self[:Contents] = text
         | 
| 301 | 
            +
                      self
         | 
| 302 | 
            +
                    end
         | 
| 303 | 
            +
                  end
         | 
| 304 | 
            +
             | 
| 305 | 
            +
                  # Describes the opacity values +fill_alpha+ and +stroke_alpha+ of an annotation.
         | 
| 306 | 
            +
                  #
         | 
| 307 | 
            +
                  # See Annotation#opacity
         | 
| 308 | 
            +
                  Opacity = Struct.new(:fill_alpha, :stroke_alpha)
         | 
| 309 | 
            +
             | 
| 310 | 
            +
                  # :call-seq:
         | 
| 311 | 
            +
                  #   annotation.opacity                                           => current_values
         | 
| 312 | 
            +
                  #   annotation.opacity(fill_alpha:)                              => annotation
         | 
| 313 | 
            +
                  #   annotation.opacity(stroke_alpha:)                            => annotation
         | 
| 314 | 
            +
                  #   annotation.opacity(fill_alpha:, stroke_alpha:)               => annotation
         | 
| 315 | 
            +
                  #
         | 
| 316 | 
            +
                  # Returns an Opacity instance representing the fill and stroke alpha values when no arguments
         | 
| 317 | 
            +
                  # are given. Otherwise sets the provided alpha values and returns self.
         | 
| 318 | 
            +
                  #
         | 
| 319 | 
            +
                  # The fill and stroke alpha values are used when regenerating the annotation's appearance
         | 
| 320 | 
            +
                  # stream and determine how opaque drawn elements will be. Note that the fill alpha value
         | 
| 321 | 
            +
                  # applies not just to fill values but to all non-stroking operations (e.g. images, ...).
         | 
| 322 | 
            +
                  def opacity(fill_alpha: nil, stroke_alpha: nil)
         | 
| 323 | 
            +
                    if !fill_alpha.nil? || !stroke_alpha.nil?
         | 
| 324 | 
            +
                      self[:CA] = stroke_alpha unless stroke_alpha.nil?
         | 
| 325 | 
            +
                      self[:ca] = fill_alpha unless fill_alpha.nil?
         | 
| 326 | 
            +
                      self
         | 
| 327 | 
            +
                    else
         | 
| 328 | 
            +
                      Opacity.new(key?(:ca) ? self[:ca] : self[:CA], self[:CA])
         | 
| 329 | 
            +
                    end
         | 
| 330 | 
            +
                  end
         | 
| 331 | 
            +
             | 
| 262 332 | 
             
                  private
         | 
| 263 333 |  | 
| 264 334 | 
             
                  def perform_validation(&block) #:nodoc:
         | 
| @@ -0,0 +1,348 @@ | |
| 1 | 
            +
            # -*- encoding: utf-8; frozen_string_literal: true -*-
         | 
| 2 | 
            +
            #
         | 
| 3 | 
            +
            #--
         | 
| 4 | 
            +
            # This file is part of HexaPDF.
         | 
| 5 | 
            +
            #
         | 
| 6 | 
            +
            # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
         | 
| 7 | 
            +
            # Copyright (C) 2014-2025 Thomas Leitner
         | 
| 8 | 
            +
            #
         | 
| 9 | 
            +
            # HexaPDF is free software: you can redistribute it and/or modify it
         | 
| 10 | 
            +
            # under the terms of the GNU Affero General Public License version 3 as
         | 
| 11 | 
            +
            # published by the Free Software Foundation with the addition of the
         | 
| 12 | 
            +
            # following permission added to Section 15 as permitted in Section 7(a):
         | 
| 13 | 
            +
            # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
         | 
| 14 | 
            +
            # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
         | 
| 15 | 
            +
            # INFRINGEMENT OF THIRD PARTY RIGHTS.
         | 
| 16 | 
            +
            #
         | 
| 17 | 
            +
            # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
         | 
| 18 | 
            +
            # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
         | 
| 19 | 
            +
            # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
         | 
| 20 | 
            +
            # License for more details.
         | 
| 21 | 
            +
            #
         | 
| 22 | 
            +
            # You should have received a copy of the GNU Affero General Public License
         | 
| 23 | 
            +
            # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
         | 
| 24 | 
            +
            #
         | 
| 25 | 
            +
            # The interactive user interfaces in modified source and object code
         | 
| 26 | 
            +
            # versions of HexaPDF must display Appropriate Legal Notices, as required
         | 
| 27 | 
            +
            # under Section 5 of the GNU Affero General Public License version 3.
         | 
| 28 | 
            +
            #
         | 
| 29 | 
            +
            # In accordance with Section 7(b) of the GNU Affero General Public
         | 
| 30 | 
            +
            # License, a covered work must retain the producer line in every PDF that
         | 
| 31 | 
            +
            # is created or manipulated using HexaPDF.
         | 
| 32 | 
            +
            #
         | 
| 33 | 
            +
            # If the GNU Affero General Public License doesn't fit your need,
         | 
| 34 | 
            +
            # commercial licenses are available at <https://gettalong.at/hexapdf/>.
         | 
| 35 | 
            +
            #++
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            require 'hexapdf/error'
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            module HexaPDF
         | 
| 40 | 
            +
              module Type
         | 
| 41 | 
            +
                module Annotations
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  # The AppearanceGenerator class provides methods for generating the appearance streams of
         | 
| 44 | 
            +
                  # annotations except those for widgets (see HexaPDF::Type::AcroForm::AppearanceGenerator for
         | 
| 45 | 
            +
                  # those).
         | 
| 46 | 
            +
                  #
         | 
| 47 | 
            +
                  # There is one private create_TYPE_appearance method for each annotation type. This allows
         | 
| 48 | 
            +
                  # subclassing the appearance generator and adjusting the appearances to one's needs.
         | 
| 49 | 
            +
                  #
         | 
| 50 | 
            +
                  # By default, an existing appearance is overwritten and the +:print+ flag is set as well as
         | 
| 51 | 
            +
                  # the +:hidden+ flag unset on the annotation so that the appearance will appear on print-outs.
         | 
| 52 | 
            +
                  #
         | 
| 53 | 
            +
                  # Also note that the annotation's /Rect entry is modified so that it contains the whole
         | 
| 54 | 
            +
                  # generated appearance.
         | 
| 55 | 
            +
                  #
         | 
| 56 | 
            +
                  # The visual appearances are chosen to be similar to those used by Adobe Acrobat and others.
         | 
| 57 | 
            +
                  # By subclassing and overriding the necessary methods it is possible to define custom
         | 
| 58 | 
            +
                  # appearances.
         | 
| 59 | 
            +
                  #
         | 
| 60 | 
            +
                  # The default annotation appearance generator for a document can be changed using the
         | 
| 61 | 
            +
                  # 'annotation.appearance_generator' configuration option.
         | 
| 62 | 
            +
                  #
         | 
| 63 | 
            +
                  # See: PDF2.0 s12.5
         | 
| 64 | 
            +
                  class AppearanceGenerator
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                    # Creates a new instance for the given +annotation+.
         | 
| 67 | 
            +
                    def initialize(annotation)
         | 
| 68 | 
            +
                      @annot = annotation
         | 
| 69 | 
            +
                      @document = annotation.document
         | 
| 70 | 
            +
                    end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                    # Creates the appropriate appearance for the annotation provided on initialization.
         | 
| 73 | 
            +
                    def create_appearance
         | 
| 74 | 
            +
                      case @annot[:Subtype]
         | 
| 75 | 
            +
                      when :Line then create_line_appearance
         | 
| 76 | 
            +
                      when :Square then create_square_circle_appearance(:square)
         | 
| 77 | 
            +
                      when :Circle then create_square_circle_appearance(:circle)
         | 
| 78 | 
            +
                      else
         | 
| 79 | 
            +
                        raise HexaPDF::Error, "Appearance regeneration for #{@annot[:Subtype]} not yet supported"
         | 
| 80 | 
            +
                      end
         | 
| 81 | 
            +
                    end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                    private
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    # Creates the appropriate appearance for a line annotation.
         | 
| 86 | 
            +
                    #
         | 
| 87 | 
            +
                    # Nearly all the needed information can be taken from the annotation object itself. However,
         | 
| 88 | 
            +
                    # the PDF specification doesn't specify where to take the font related information (font,
         | 
| 89 | 
            +
                    # size, alignment...) from. Therefore this is currently hard-coded as left-aligned Helvetica
         | 
| 90 | 
            +
                    # in size 9.
         | 
| 91 | 
            +
                    #
         | 
| 92 | 
            +
                    # There are also some other decisions that are left to the implementation, like padding
         | 
| 93 | 
            +
                    # around the annotation or the size of the line ending shapes. Those are implemented to be
         | 
| 94 | 
            +
                    # similar to how viewers create the appearance.
         | 
| 95 | 
            +
                    #
         | 
| 96 | 
            +
                    # See: HexaPDF::Type::Annotations::Line
         | 
| 97 | 
            +
                    def create_line_appearance
         | 
| 98 | 
            +
                      # Prepare the annotation
         | 
| 99 | 
            +
                      form = (@annot[:AP] ||= {})[:N] ||=
         | 
| 100 | 
            +
                        @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 0, 0]})
         | 
| 101 | 
            +
                      form.contents = ""
         | 
| 102 | 
            +
                      @annot.flag(:print)
         | 
| 103 | 
            +
                      @annot.unflag(:hidden)
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                      # Get or calculate all needed values from the annotation
         | 
| 106 | 
            +
                      x0, y0, x1, y1 = @annot.line
         | 
| 107 | 
            +
                      style = @annot.border_style
         | 
| 108 | 
            +
                      line_ending_style = @annot.line_ending_style
         | 
| 109 | 
            +
                      opacity = @annot.opacity
         | 
| 110 | 
            +
                      ll = @annot.leader_line_length
         | 
| 111 | 
            +
                      lle = @annot.leader_line_extension_length
         | 
| 112 | 
            +
                      llo = @annot.leader_line_offset
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                      angle = Math.atan2(y1 - y0, x1 - x0)
         | 
| 115 | 
            +
                      cos_angle = Math.cos(angle)
         | 
| 116 | 
            +
                      sin_angle = Math.sin(angle)
         | 
| 117 | 
            +
                      line_length = Math.sqrt((y1 - y0) ** 2 + (x1 - x0) ** 2)
         | 
| 118 | 
            +
                      ll_sign = (ll > 0 ? 1 : -1)
         | 
| 119 | 
            +
                      ll_y = ll_sign * (ll.abs + lle + llo)
         | 
| 120 | 
            +
                      line_y = (ll != 0 ? ll_sign * (llo + ll.abs) : 0)
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                      captioned = @annot.captioned
         | 
| 123 | 
            +
                      contents = @annot.contents.to_s
         | 
| 124 | 
            +
                      if captioned && !contents.empty?
         | 
| 125 | 
            +
                        cap_position = @annot.caption_position
         | 
| 126 | 
            +
                        cap_style = HexaPDF::Layout::Style.new(font: 'Helvetica', font_size: 9,
         | 
| 127 | 
            +
                                                               fill_color: style.color || 'black',
         | 
| 128 | 
            +
                                                               line_spacing: 1.25)
         | 
| 129 | 
            +
                        cap_items = @document.layout.text_fragments(contents, style: cap_style)
         | 
| 130 | 
            +
                        layouter = Layout::TextLayouter.new(cap_style)
         | 
| 131 | 
            +
                        cap_result = layouter.fit(cap_items, 2**20, 2**20)
         | 
| 132 | 
            +
                        cap_width = cap_result.lines.max_by(&:width).width + 2 # for padding left/right
         | 
| 133 | 
            +
                        cap_offset = @annot.caption_offset
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                        cap_x = (line_length - cap_width) / 2.0 + cap_offset[0]
         | 
| 136 | 
            +
                        # Note that the '+ 2' is just so that there is a small gap to the line
         | 
| 137 | 
            +
                        cap_y = line_y + cap_offset[1] +
         | 
| 138 | 
            +
                                (cap_position == :inline ? cap_result.height / 2.0 : cap_result.height + 2)
         | 
| 139 | 
            +
                      end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                      # Calculate annotation rectangle and form bounding box. This considers the line's start
         | 
| 142 | 
            +
                      # and end points as well as the end points of the leader lines, the line ending style and
         | 
| 143 | 
            +
                      # the caption when calculating the bounding box.
         | 
| 144 | 
            +
                      #
         | 
| 145 | 
            +
                      # The result could still be improved by tailoring to the specific line ending style.
         | 
| 146 | 
            +
                      calculate_le_padding = lambda do |le_style|
         | 
| 147 | 
            +
                        case le_style
         | 
| 148 | 
            +
                        when :square, :circle, :diamond, :slash, :open_arrow, :closed_arrow
         | 
| 149 | 
            +
                          3 * style.width
         | 
| 150 | 
            +
                        when :ropen_arrow, :rclosed_arrow
         | 
| 151 | 
            +
                          10 * style.width
         | 
| 152 | 
            +
                        else
         | 
| 153 | 
            +
                          0
         | 
| 154 | 
            +
                        end
         | 
| 155 | 
            +
                      end
         | 
| 156 | 
            +
                      dstart = calculate_le_padding.call(line_ending_style.start_style)
         | 
| 157 | 
            +
                      dend = calculate_le_padding.call(line_ending_style.end_style)
         | 
| 158 | 
            +
                      if captioned
         | 
| 159 | 
            +
                        cap_ulx = x0 + cos_angle * cap_x - sin_angle * cap_y
         | 
| 160 | 
            +
                        cap_uly = y0 + sin_angle * cap_x + cos_angle * cap_y
         | 
| 161 | 
            +
                      end
         | 
| 162 | 
            +
                      min_x, max_x = [x0, x0 - sin_angle * ll_y, x0 - sin_angle * line_y - cos_angle * dstart,
         | 
| 163 | 
            +
                                      x1, x1 - sin_angle * ll_y, x1 - sin_angle * line_y + cos_angle * dend,
         | 
| 164 | 
            +
                                      *([cap_ulx,
         | 
| 165 | 
            +
                                         cap_ulx + cos_angle * cap_width,
         | 
| 166 | 
            +
                                         cap_ulx - sin_angle * cap_result.height,
         | 
| 167 | 
            +
                                         cap_ulx + cos_angle * cap_width - sin_angle * cap_result.height
         | 
| 168 | 
            +
                                        ] if captioned)
         | 
| 169 | 
            +
                                     ].minmax
         | 
| 170 | 
            +
                      min_y, max_y = [y0, y0 + cos_angle * ll_y,
         | 
| 171 | 
            +
                                      y0 + cos_angle * line_y - ([cos_angle, sin_angle].max) * dstart,
         | 
| 172 | 
            +
                                      y1, y1 + cos_angle * ll_y,
         | 
| 173 | 
            +
                                      y1 + cos_angle * line_y + ([cos_angle, sin_angle].max) * dend,
         | 
| 174 | 
            +
                                      *([cap_uly,
         | 
| 175 | 
            +
                                         cap_uly + sin_angle * cap_width,
         | 
| 176 | 
            +
                                         cap_uly - cos_angle * cap_result.height,
         | 
| 177 | 
            +
                                         cap_uly + sin_angle * cap_width - cos_angle * cap_result.height
         | 
| 178 | 
            +
                                        ] if captioned)
         | 
| 179 | 
            +
                                     ].minmax
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                      padding = 4 * style.width
         | 
| 182 | 
            +
                      rect = [min_x - padding, min_y - padding, max_x + padding, max_y + padding]
         | 
| 183 | 
            +
                      @annot[:Rect] = rect
         | 
| 184 | 
            +
                      form[:BBox] = rect.dup
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                      # Set the appropriate graphics state and transform the canvas so that the line is
         | 
| 187 | 
            +
                      # unrotated and its start point at the origin.
         | 
| 188 | 
            +
                      canvas = form.canvas(translate: false)
         | 
| 189 | 
            +
                      canvas.opacity(**opacity.to_h)
         | 
| 190 | 
            +
                      canvas.stroke_color(style.color) if style.color
         | 
| 191 | 
            +
                      canvas.fill_color(@annot.interior_color) if @annot.interior_color
         | 
| 192 | 
            +
                      canvas.line_width(style.width)
         | 
| 193 | 
            +
                      canvas.line_dash_pattern(style.style) if style.style.kind_of?(Array)
         | 
| 194 | 
            +
                      canvas.transform(cos_angle, sin_angle, -sin_angle, cos_angle, x0, y0)
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                      stroke_op = (style.color ? :stroke : :end_path)
         | 
| 197 | 
            +
                      fill_op = (style.color && @annot.interior_color ? :fill_stroke :
         | 
| 198 | 
            +
                                   (style.color ? :stroke : :fill))
         | 
| 199 | 
            +
             | 
| 200 | 
            +
                      # Draw leader lines and line
         | 
| 201 | 
            +
                      if ll != 0
         | 
| 202 | 
            +
                        canvas.line(0, ll_sign * llo, 0, ll_y)
         | 
| 203 | 
            +
                        canvas.line(line_length, ll_sign * llo, line_length, ll_y)
         | 
| 204 | 
            +
                      end
         | 
| 205 | 
            +
                      if captioned && cap_position == :inline
         | 
| 206 | 
            +
                        canvas.line(0, line_y, [[0, cap_x].max, line_length].min, line_y)
         | 
| 207 | 
            +
                        canvas.line([[cap_x + cap_width, 0].max, line_length].min, line_y, line_length, line_y)
         | 
| 208 | 
            +
                      else
         | 
| 209 | 
            +
                        canvas.line(0, line_y, line_length, line_y)
         | 
| 210 | 
            +
                      end
         | 
| 211 | 
            +
                      canvas.send(stroke_op)
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                      # Draw line endings
         | 
| 214 | 
            +
                      if line_ending_style.start_style != :none
         | 
| 215 | 
            +
                        do_fill = draw_line_ending(canvas, line_ending_style.start_style, 0, line_y,
         | 
| 216 | 
            +
                                                   style.width, 0)
         | 
| 217 | 
            +
                        canvas.send(do_fill ? fill_op : stroke_op)
         | 
| 218 | 
            +
                      end
         | 
| 219 | 
            +
                      if line_ending_style.end_style != :none
         | 
| 220 | 
            +
                        do_fill = draw_line_ending(canvas, line_ending_style.end_style, line_length, line_y,
         | 
| 221 | 
            +
                                                   style.width, Math::PI)
         | 
| 222 | 
            +
                        canvas.send(do_fill ? fill_op : stroke_op)
         | 
| 223 | 
            +
                      end
         | 
| 224 | 
            +
             | 
| 225 | 
            +
                      # Draw caption, adding half of the padding added to cap_width
         | 
| 226 | 
            +
                      cap_result.draw(canvas, cap_x + 1, cap_y) if captioned
         | 
| 227 | 
            +
                    end
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                    # Creates the appropriate appearance for a square or circle annotation depending on the
         | 
| 230 | 
            +
                    # given +type+ (which can either be +:square+ or +:circle+).
         | 
| 231 | 
            +
                    #
         | 
| 232 | 
            +
                    # The cloudy border effect is not supported.
         | 
| 233 | 
            +
                    #
         | 
| 234 | 
            +
                    # See: HexaPDF::Type::Annotations::Square, HexaPDF::Type::Annotations::Circle
         | 
| 235 | 
            +
                    def create_square_circle_appearance(type)
         | 
| 236 | 
            +
                      # Prepare the annotation
         | 
| 237 | 
            +
                      form = (@annot[:AP] ||= {})[:N] ||=
         | 
| 238 | 
            +
                        @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 0, 0]})
         | 
| 239 | 
            +
                      form.contents = ""
         | 
| 240 | 
            +
                      @annot.flag(:print)
         | 
| 241 | 
            +
                      @annot.unflag(:hidden)
         | 
| 242 | 
            +
             | 
| 243 | 
            +
                      rect = @annot[:Rect]
         | 
| 244 | 
            +
                      x, y, w, h = rect.left, rect.bottom, rect.width, rect.height
         | 
| 245 | 
            +
                      border_style = @annot.border_style
         | 
| 246 | 
            +
                      interior_color = @annot.interior_color
         | 
| 247 | 
            +
                      opacity = @annot.opacity
         | 
| 248 | 
            +
             | 
| 249 | 
            +
                      # Take the differences array into account. If it exists, the boundary of the actual
         | 
| 250 | 
            +
                      # rectangle is the one with the differences applied to /Rect.
         | 
| 251 | 
            +
                      #
         | 
| 252 | 
            +
                      # If the differences array doesn't exist, we assume that the /Rect is the rectangle we
         | 
| 253 | 
            +
                      # want to draw, with the line width split on both side (like with Canvas#rectangle). In
         | 
| 254 | 
            +
                      # this case we need to update /Rect accordingly so that the line width on the outside is
         | 
| 255 | 
            +
                      # correctly shown.
         | 
| 256 | 
            +
                      line_width_adjustment = border_style.width / 2.0
         | 
| 257 | 
            +
                      if (rd = @annot[:RD])
         | 
| 258 | 
            +
                        x += rd[0]
         | 
| 259 | 
            +
                        y += rd[3]
         | 
| 260 | 
            +
                        w -= rd[0] + rd[2]
         | 
| 261 | 
            +
                        h -= rd[1] + rd[3]
         | 
| 262 | 
            +
                      else
         | 
| 263 | 
            +
                        @annot[:RD] = [0, 0, 0, 0]
         | 
| 264 | 
            +
                        x = rect.left -= line_width_adjustment
         | 
| 265 | 
            +
                        y = rect.bottom -= line_width_adjustment
         | 
| 266 | 
            +
                        w = rect.width += line_width_adjustment
         | 
| 267 | 
            +
                        h = rect.height += line_width_adjustment
         | 
| 268 | 
            +
                      end
         | 
| 269 | 
            +
                      x += line_width_adjustment
         | 
| 270 | 
            +
                      y += line_width_adjustment
         | 
| 271 | 
            +
                      w -= 2 * line_width_adjustment
         | 
| 272 | 
            +
                      h -= 2 * line_width_adjustment
         | 
| 273 | 
            +
             | 
| 274 | 
            +
                      x -= rect.left
         | 
| 275 | 
            +
                      y -= rect.bottom
         | 
| 276 | 
            +
                      form[:BBox] = [0, 0, rect.width, rect.height]
         | 
| 277 | 
            +
             | 
| 278 | 
            +
                      if border_style.color || interior_color
         | 
| 279 | 
            +
                        canvas = form.canvas
         | 
| 280 | 
            +
                        canvas.opacity(**opacity.to_h)
         | 
| 281 | 
            +
                        canvas.stroke_color(border_style.color) if border_style.color
         | 
| 282 | 
            +
                        canvas.fill_color(interior_color) if interior_color
         | 
| 283 | 
            +
                        canvas.line_width(border_style.width)
         | 
| 284 | 
            +
                        canvas.line_dash_pattern(border_style.style) if border_style.style.kind_of?(Array)
         | 
| 285 | 
            +
             | 
| 286 | 
            +
                        if type == :square
         | 
| 287 | 
            +
                          canvas.rectangle(x, y, w, h)
         | 
| 288 | 
            +
                        else
         | 
| 289 | 
            +
                          canvas.ellipse(x + w / 2.0, y + h / 2.0, a: w / 2.0, b: h / 2.0)
         | 
| 290 | 
            +
                        end
         | 
| 291 | 
            +
             | 
| 292 | 
            +
                        if border_style.color && interior_color
         | 
| 293 | 
            +
                          canvas.fill_stroke
         | 
| 294 | 
            +
                        elsif border_style.color
         | 
| 295 | 
            +
                          canvas.stroke
         | 
| 296 | 
            +
                        else
         | 
| 297 | 
            +
                          canvas.fill
         | 
| 298 | 
            +
                        end
         | 
| 299 | 
            +
                      end
         | 
| 300 | 
            +
                    end
         | 
| 301 | 
            +
             | 
| 302 | 
            +
                    # Draws the line ending style +type+ at the position (+x+, +y+) and returns +true+ if the
         | 
| 303 | 
            +
                    # shape needs to be filled.
         | 
| 304 | 
            +
                    #
         | 
| 305 | 
            +
                    # The argument +angle+ specifies the angle at which the line ending style should be drawn.
         | 
| 306 | 
            +
                    #
         | 
| 307 | 
            +
                    # The +line_width+ is needed because the size of the line ending depends on it.
         | 
| 308 | 
            +
                    def draw_line_ending(canvas, type, x, y, line_width, angle)
         | 
| 309 | 
            +
                      lw3 = 3 * line_width
         | 
| 310 | 
            +
             | 
| 311 | 
            +
                      case type
         | 
| 312 | 
            +
                      when :square
         | 
| 313 | 
            +
                        canvas.rectangle(x - lw3, y - lw3, 2 * lw3, 2 * lw3)
         | 
| 314 | 
            +
                        true
         | 
| 315 | 
            +
                      when :circle
         | 
| 316 | 
            +
                        canvas.circle(x, y, lw3)
         | 
| 317 | 
            +
                        true
         | 
| 318 | 
            +
                      when :diamond
         | 
| 319 | 
            +
                        canvas.polygon(x + lw3, y, x, y + lw3, x - lw3, y, x, y - lw3)
         | 
| 320 | 
            +
                        true
         | 
| 321 | 
            +
                      when :open_arrow, :closed_arrow, :ropen_arrow, :rclosed_arrow
         | 
| 322 | 
            +
                        arrow_cos = Math.cos(Math::PI / 6 + angle)
         | 
| 323 | 
            +
                        arrow_sin = Math.sin(Math::PI / 6 + angle)
         | 
| 324 | 
            +
                        dir = (type == :ropen_arrow || type == :rclosed_arrow ? -1 : 1)
         | 
| 325 | 
            +
                        canvas.polyline(x + dir * arrow_cos * 3 * lw3, y + arrow_sin * 3 * lw3, x, y,
         | 
| 326 | 
            +
                                        x + dir * arrow_cos * 3 * lw3, y - arrow_sin * 3 * lw3)
         | 
| 327 | 
            +
                        if type == :closed_arrow || type == :rclosed_arrow
         | 
| 328 | 
            +
                          canvas.close_subpath
         | 
| 329 | 
            +
                          true
         | 
| 330 | 
            +
                        else
         | 
| 331 | 
            +
                          false
         | 
| 332 | 
            +
                        end
         | 
| 333 | 
            +
                      when :butt
         | 
| 334 | 
            +
                        canvas.line(x, y + lw3, x, y - lw3)
         | 
| 335 | 
            +
                        false
         | 
| 336 | 
            +
                      when :slash
         | 
| 337 | 
            +
                        sin_60 = Math.sin(Math::PI / 3)
         | 
| 338 | 
            +
                        cos_60 = Math.cos(Math::PI / 3)
         | 
| 339 | 
            +
                        canvas.line(x + cos_60 * lw3, y + sin_60 * lw3, x - cos_60 * lw3, y - sin_60 * lw3)
         | 
| 340 | 
            +
                        false
         | 
| 341 | 
            +
                      end
         | 
| 342 | 
            +
                    end
         | 
| 343 | 
            +
             | 
| 344 | 
            +
                  end
         | 
| 345 | 
            +
             | 
| 346 | 
            +
                end
         | 
| 347 | 
            +
              end
         | 
| 348 | 
            +
            end
         |