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).

Note: This class should not be used directly but via HexaPDF::Document::Layout#text_fragments. This way the whole document layout infrastructure like font fallback and such is automatically used.

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, properties: nil) ⇒ 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 properties, see Style::create for details.

For internal use, see the note under TextFragment for details.



176
177
178
179
180
# File 'lib/hexapdf/layout/text_fragment.rb', line 176

def initialize(items, style, properties: nil)
  @items = items
  @style = Style.create(style)
  @properties = properties
end

Instance Attribute Details

#itemsObject

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



140
141
142
# File 'lib/hexapdf/layout/text_fragment.rb', line 140

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#underlays
  • Style#overlays


168
169
170
# File 'lib/hexapdf/layout/text_fragment.rb', line 168

def style
  @style
end

Class Method Details

.create(text, style) ⇒ Object

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

The needed style of the text fragment is specified by the style argument (see Style::create for details). Note that the resulting style object needs at least the font set.

For internal use, see the note under TextFragment for details.



73
74
75
76
77
# File 'lib/hexapdf/layout/text_fragment.rb', line 73

def self.create(text, style)
  style = Style.create(style)
  fragment = new(style.font.decode_utf8(text), style)
  TextShaper.new.shape_text(fragment)
end

.create_with_fallback_glyphs(text, style) ⇒ Object

:call-seq:

TextFragment.create_with_fallback_glyphs(text, style)                      -> [frag]
TextFragment.create_with_fallback_glyphs(text, style) {|codepoint| block } -> [frag1, frag2, ...]

Creates one or more TextFragment objects for the given text - possibly using glyphs from fallback fonts -, shapes them and returns them.

If no block is given, the method works like #create but returns the text fragment inside an array.

If a block is given, the text is split on codepoints for which there is no glyph in the style's font. For the parts with valid glyphs TextFragment objects are created like with #create. Each codepoint without a valid glyph is yielded to the given block together with the associated HexaPDF::Font::InvalidGlyph object as arguments. The block needs to return an array of either HexaPDF::Font::Type1Wrapper::Glyph or HexaPDF::Font::TrueTypeWrapper::Glyph objects. This array is then used for creating a TextFragment object.

The needed style of the text fragments is specified by the style argument (see Style::create for details). Note that the resulting style object needs at least the font set.

For internal use, see the note under TextFragment for details.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/hexapdf/layout/text_fragment.rb', line 101

def self.create_with_fallback_glyphs(text, style)
  return [create(text, style)] if !block_given? || text.empty?

  style = Style.create(style)
  styles = Hash.new {|h, k| h[k] = style.dup.font(k) }
  styles[style.font] = style

  result = []
  items = []
  shaper = TextShaper.new
  font = style.font
  text.each_codepoint do |codepoint|
    glyph = font.decode_codepoint(codepoint)
    if glyph.valid?
      items << glyph
    else
      unless items.empty?
        result.append(*shaper.shape_text(new(items, style)))
        items = []
      end
      if glyph.control_char?
        if result.last&.style == style
          result.last.items << glyph
        else
          result.append(new([glyph], style))
        end
      else
        fallback = yield(codepoint, glyph)
        unless fallback.empty?
          result.append(*shaper.shape_text(new(fallback, styles[fallback.first.font_wrapper])))
        end
      end
    end
  end
  result.append(*shaper.shape_text(new(items, style))) unless items.empty?
  result
end

Instance Method Details

#attributes_hashObject

Returns the value that should be used as hash key when only the fragment's attributes - without the items - should play a role.



202
203
204
# File 'lib/hexapdf/layout/text_fragment.rb', line 202

def attributes_hash
  @style.hash ^ @properties.hash
end

#clear_cacheObject

Clears all cached values.

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



387
388
389
390
# File 'lib/hexapdf/layout/text_fragment.rb', line 387

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.



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/hexapdf/layout/text_fragment.rb', line 228

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

  in_text_object = (canvas.graphics_object == :text)
  canvas.begin_text
  tlm = canvas.graphics_state.tlm
  tx = x - tlm.e
  ty = y - tlm.f
  if tx.abs < PRECISION
    if ty.abs < PRECISION
      # do nothing
    elsif (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], absolute: in_text_object)
  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

#dup_attributes(items) ⇒ Object

Creates a new TextFragment with the same style and custom properties as this one but with the given items.



189
190
191
# File 'lib/hexapdf/layout/text_fragment.rb', line 189

def dup_attributes(items)
  self.class.new(items, @style, properties: @properties.dup)
end

#exact_y_maxObject

The maximum y-coordinate of any item.



330
331
332
333
# File 'lib/hexapdf/layout/text_fragment.rb', line 330

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.



324
325
326
327
# File 'lib/hexapdf/layout/text_fragment.rb', line 324

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

#fill_horizontal!(width) ⇒ Object

Creates a new text fragment that repeats this fragment's items and applies the necessary spacing so that the returned text fragment fills the given width completely.

If the given width is less than the fragment's width, self is returned.



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/hexapdf/layout/text_fragment.rb', line 366

def fill_horizontal!(width)
  return self if width < self.width

  factor, rest = width.divmod(self.width)
  items = @items * factor
  rest = @items.inject(rest) do |available_width, item|
    new_available_width = available_width - style.scaled_item_width(item)
    break available_width if new_available_width < 0
    items << item
    new_available_width
  end

  spacing = rest / (items.size - 1)
  new_style = @style.dup.update(character_spacing: spacing)
  items << spacing / new_style.scaled_font_size # correct spacing after last item
  self.class.new(items, new_style, properties: @properties.dup)
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.



351
352
353
# File 'lib/hexapdf/layout/text_fragment.rb', line 351

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

#inspectObject

:nodoc:



393
394
395
# File 'lib/hexapdf/layout/text_fragment.rb', line 393

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

#propertiesObject

Returns the custom properties hash for the text fragment.

See Box#properties for usage details.



196
197
198
# File 'lib/hexapdf/layout/text_fragment.rb', line 196

def properties
  @properties ||= {}
end

#textObject

Returns the text of the fragment.



183
184
185
# File 'lib/hexapdf/layout/text_fragment.rb', line 183

def text
  items.reject {|i| i.kind_of?(Numeric) }.map(&:str).join
end

#valignObject

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

See Line for details.



358
359
360
# File 'lib/hexapdf/layout/text_fragment.rb', line 358

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 PDF2.0 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.



340
341
342
# File 'lib/hexapdf/layout/text_fragment.rb', line 340

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

#x_maxObject

The maximum x-coordinate of the last glyph.



309
310
311
# File 'lib/hexapdf/layout/text_fragment.rb', line 309

def x_max
  @x_max ||= calculate_x_max
end

#x_minObject

The minimum x-coordinate of the first glyph.



304
305
306
# File 'lib/hexapdf/layout/text_fragment.rb', line 304

def x_min
  @x_min ||= calculate_x_min
end

#y_maxObject

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



319
320
321
# File 'lib/hexapdf/layout/text_fragment.rb', line 319

def y_max
  style.scaled_y_max
end

#y_minObject

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



314
315
316
# File 'lib/hexapdf/layout/text_fragment.rb', line 314

def y_min
  style.scaled_y_min
end