Class: HexaPDF::Layout::TextFragment

Inherits:
Object
  • Object
show all
Defined in:
lib/hexapdf/layout/text_fragment.rb

Overview

A TextFragment describes an optionally kerned piece of text that shares the same font, font size and other properties.

Its items are either glyph objects of the font or numeric values describing kerning information. All returned measurement values are in text space units. If the items or the style are changed, the #clear_cache has to be called. Otherwise the measurements may not be correct!

The items of a text fragment may be frozen to indicate that the fragment is potentially used multiple times.

The rectangle with the bottom left corner (#x_min, #y_min) and the top right corner (#x_max, #y_max) describes the minimum bounding box of the whole text fragment and is usually not equal to the box (0, 0)-(#width, #height).

Constant Summary collapse

PRECISION =

The precision used to determine whether two floats represent the same value.

0.000001

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(items, style) ⇒ TextFragment

Creates a new TextFragment object with the given items and style.

The argument style can either be a Style object or a hash of style options.



104
105
106
107
# File 'lib/hexapdf/layout/text_fragment.rb', line 104

def initialize(items, style)
  @items = items
  @style = (style.kind_of?(Style) ? style : Style.new(style))
end

Instance Attribute Details

#itemsObject

The items (glyphs and kerning values) of the text fragment.



71
72
73
# File 'lib/hexapdf/layout/text_fragment.rb', line 71

def items
  @items
end

#styleObject (readonly)

The style to be applied.

Only the following properties are used:

  • Style#font

  • Style#font_size

  • Style#horizontal_scaling

  • Style#character_spacing

  • Style#word_spacing

  • Style#text_rise

  • Style#text_rendering_mode

  • Style#subscript

  • Style#superscript

  • Style#underline

  • Style#strikeout

  • Style#fill_color

  • Style#fill_alpha

  • Style#stroke_color

  • Style#stroke_alpha

  • Style#stroke_width

  • Style#stroke_cap_style

  • Style#stroke_join_style

  • Style#stroke_miter_limit

  • Style#stroke_dash_pattern

  • Style#underlay_callback

  • Style#overlay_callback



99
100
101
# File 'lib/hexapdf/layout/text_fragment.rb', line 99

def style
  @style
end

Class Method Details

.create(text, style = nil, **options) ⇒ Object

Creates a new TextFragment object for the given text, shapes it and returns it.

The needed style of the text fragment can either be specified by the style argument or via the options (in which case a new Style object is created). Regardless of the way, the resulting style object needs at least the font set.



64
65
66
67
68
# File 'lib/hexapdf/layout/text_fragment.rb', line 64

def self.create(text, style = nil, **options)
  style = (style.nil? ? Style.new(options) : style)
  fragment = new(style.font.decode_utf8(text), style)
  TextShaper.new.shape_text(fragment)
end

Instance Method Details

#clear_cacheObject

Clears all cached values.

This method needs to be called if the fragment’s items or attributes are changed!



265
266
267
268
# File 'lib/hexapdf/layout/text_fragment.rb', line 265

def clear_cache
  @x_min = @x_max = @exact_y_min = @exact_y_max = @width = @height = nil
  self
end

#draw(canvas, x, y, ignore_text_properties: false) ⇒ Object

Draws the text onto the canvas at the given position.

This method is the main styled text drawing facility and therefore some optimizations are done:

  • The text is drawn using HexaPDF::Content;:Canvas#show_glyphs_only which means that the text matrix is not updated. Therefore the caller must not rely on it!

  • All text style properties mentioned in the description of #style are set except if ignore_text_properties is set to true. Note that this only applies to style properties that directly affect text drawing, so, for example, underlays/overlays and underlining/strikeout is always done.

    The caller should set ignore_text_properties to true if the graphics state hasn’t been changed. This is the case, for example, if the last thing drawn was a text fragment with the same style.

  • It is assumed that the text matrix is not rotated, skewed, etc. so that setting the text position can be done using the optimal method.



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/hexapdf/layout/text_fragment.rb', line 131

def draw(canvas, x, y, ignore_text_properties: false)
  style.underlays.draw(canvas, x, y + y_min, self) if style.underlays?

  # Set general font related graphics state if necessary
  unless ignore_text_properties
    canvas.font(style.font, size: style.calculated_font_size).
      horizontal_scaling(style.horizontal_scaling).
      character_spacing(style.character_spacing).
      word_spacing(style.word_spacing).
      text_rise(style.calculated_text_rise).
      text_rendering_mode(style.text_rendering_mode)

    # Set fill and/or stroke related graphics state
    canvas.opacity(fill_alpha: style.fill_alpha, stroke_alpha: style.stroke_alpha)
    trm = canvas.text_rendering_mode
    if trm.value.even? # text is filled
      canvas.fill_color(style.fill_color)
    end
    if trm == :stroke || trm == :fill_stroke || trm == :stroke_clip || trm == :fill_stroke_clip
      canvas.stroke_color(style.stroke_color).
        line_width(style.stroke_width).
        line_cap_style(style.stroke_cap_style).
        line_join_style(style.stroke_join_style).
        miter_limit(style.stroke_miter_limit).
        line_dash_pattern(style.stroke_dash_pattern)
    end
  end

  canvas.begin_text
  tlm = canvas.graphics_state.tlm
  tx = x - tlm.e
  ty = y - tlm.f
  if tx.abs < PRECISION
    if (ty + canvas.graphics_state.leading).abs < PRECISION
      canvas.move_text_cursor
    else
      canvas.move_text_cursor(offset: [0, ty], absolute: false)
    end
  elsif ty.abs < PRECISION
    canvas.move_text_cursor(offset: [tx, 0], absolute: false)
  else
    canvas.move_text_cursor(offset: [x, y])
  end
  canvas.show_glyphs_only(items)

  if style.underline? && style.underline
    y_offset = style.calculated_underline_position
    canvas.save_graphics_state do
      canvas.stroke_color(style.fill_color).
        line_width(style.calculated_underline_thickness).
        line_cap_style(:butt).
        line_dash_pattern(0).
        line(x, y + y_offset, x + width, y + y_offset).
        stroke
    end
  end

  if style.strikeout? && style.strikeout
    y_offset = style.calculated_strikeout_position
    canvas.save_graphics_state do
      canvas.stroke_color(style.fill_color).
        line_width(style.calculated_strikeout_thickness).
        line_cap_style(:butt).
        line_dash_pattern(0).
        line(x, y + y_offset, x + width, y + y_offset).
        stroke
    end
  end

  style.overlays.draw(canvas, x, y + y_min, self) if style.overlays?
end

#exact_y_maxObject

The maximum y-coordinate of any item.



230
231
232
233
# File 'lib/hexapdf/layout/text_fragment.rb', line 230

def exact_y_max
  @exact_y_max ||= (@items.max_by(&:y_max)&.y_max || 0) *
    style.calculated_font_size / 1000.0 + style.calculated_text_rise
end

#exact_y_minObject

The minimum y-coordinate of any item.



224
225
226
227
# File 'lib/hexapdf/layout/text_fragment.rb', line 224

def exact_y_min
  @exact_y_min ||= (@items.min_by(&:y_min)&.y_min || 0) *
    style.calculated_font_size / 1000.0 + style.calculated_text_rise
end

#heightObject

The height of the text fragment.

It is calculated as the difference of the maximum of the y_max values and the minimum of the y_min values of the items. However, the text rise value is also taken into account so that the baseline is always inside the bounds. For example, if a large negative text rise value is used, the baseline will be equal to the top boundary; if a large positive value is used, it will be equal to the bottom boundary.



251
252
253
# File 'lib/hexapdf/layout/text_fragment.rb', line 251

def height
  @height ||= [y_max, 0].max - [y_min, 0].min
end

#inspectObject

:nodoc:



271
272
273
# File 'lib/hexapdf/layout/text_fragment.rb', line 271

def inspect
  "#<#{self.class.name} #{items.inspect}>"
end

#valignObject

Returns the vertical alignment inside a line which is always :text for text fragments.

See Line for details.



258
259
260
# File 'lib/hexapdf/layout/text_fragment.rb', line 258

def valign
  :text
end

#widthObject

The width of the text fragment.

It is the sum of the widths of its items and is calculated by using the algorithm presented in PDF1.7 s9.4.4. By using kerning values as the first and/or last items, the text contained in the fragment may spill over the left and/or right boundary.



240
241
242
# File 'lib/hexapdf/layout/text_fragment.rb', line 240

def width
  @width ||= @items.sum {|item| style.scaled_item_width(item) }
end

#x_maxObject

The maximum x-coordinate of the last glyph.



209
210
211
# File 'lib/hexapdf/layout/text_fragment.rb', line 209

def x_max
  @x_max ||= calculate_x_max
end

#x_minObject

The minimum x-coordinate of the first glyph.



204
205
206
# File 'lib/hexapdf/layout/text_fragment.rb', line 204

def x_min
  @x_min ||= calculate_x_min
end

#y_maxObject

The maximum y-coordinate, calculated using the scaled ascender of the font.



219
220
221
# File 'lib/hexapdf/layout/text_fragment.rb', line 219

def y_max
  style.scaled_y_max
end

#y_minObject

The minimum y-coordinate, calculated using the scaled descender of the font.



214
215
216
# File 'lib/hexapdf/layout/text_fragment.rb', line 214

def y_min
  style.scaled_y_min
end