Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emoji cause text to extend beyond edge of bounding box #7

Open
xtian opened this issue Feb 1, 2016 · 12 comments
Open

Emoji cause text to extend beyond edge of bounding box #7

xtian opened this issue Feb 1, 2016 · 12 comments

Comments

@xtian
Copy link

xtian commented Feb 1, 2016

Rendering an emoji in a line causes it to extend beyond the boundaries of its containing bounding box.

Example:

screen shot 2016-02-01 at 2 03 13 pm
screen shot 2016-02-01 at 2 02 25 pm

@hidakatsuya
Copy link
Owner

@xtian Can you show me code of the example?

@xtian
Copy link
Author

xtian commented Feb 2, 2016

@hidakatsuya, here's the code where this is happening in my project:

@document.bounding_box [0.9.in, 1.1.in], { width: 6.in, height: 0.7.in } do
  @document.font 'Sentinel Book', size: 9 do
    @document.text text, align: :center, valign: :center, leading: 2, inline_format: true
  end
end

@hidakatsuya
Copy link
Owner

@xtian Thanks so much!

@hidakatsuya
Copy link
Owner

This problem is very difficult. There is no easy implementation to fix, but you may be able to resolve to use a Japanese font like ipag.ttf.

# Install ipag.ttf
@document.font_families.update('IPAGothic' => { normal: 'ipag.ttf' })
# :
@document.text 'foo bar <font name="IPAGothic">😀</font>', inline_format: true

@xtian
Copy link
Author

xtian commented Mar 20, 2016

Yeah, I looked into fixing this myself and couldn't find a straightforward way to do it. Frustrating!

Thanks for the Japanese font tip. I'm no longer working on the project where I was having this issue, but maybe it will help someone else.

Feel free to close this issue if you'd like.

@hidakatsuya hidakatsuya added the bug Something isn't working label Mar 21, 2016
@hidakatsuya hidakatsuya added known issue and removed bug Something isn't working labels Jan 3, 2018
@aried3r
Copy link
Contributor

aried3r commented Feb 28, 2020

Hey! We're also facing this issue and were wondering if you have maybe some details where and why exactly this is happening, @hidakatsuya.

@hidakatsuya
Copy link
Owner

OK, please wait for a while as I will investigate this issue again.

@brandoncc
Copy link

Did you ever have a chance to look into this again?

@gettalong
Copy link

@aried3r @brandoncc @hidakatsuya This most certainly happens due to the line wrapping algorithm not knowing the size of the emoji images together with the missing width information for the emoji characters.

@brandoncc
Copy link

Thanks @gettalong!

@ggmomo
Copy link

ggmomo commented Nov 22, 2021

I managed to make it work by overriding some methods from Prawn 2.3.0. The idea was to replace emojis with classic chars so Prawn manages to wrap lines correctly.
⚠️ Ipag ttf font must be set up when initializing a Prawn document.

Here is my (naive) proposal:

Requirements

  • prawn 2.3.0
  • prawn-emoji 5.0.0
  • unicode-emoji
  • ipag font

Prawn emoji

Fix cursor position by adding document.font_size instead of emoji_image.width.

Prawn::Emoji::Drawer.class_eval do
  def draw_emoji(emoji_char, at:)
    emoji_image = Emoji::Image.new(emoji_char)
    emoji_image.render(document, at: at)
  
    document.font_size + document.character_spacing
  end
end

Prawn

Copy this code into config/intitializers if running a Rails app.

# Replace emojis unicodes with char to fix the caclculation of the length
# you should use the same unicode emoji regex as the one set up in your prawn-emoji config
def replace_emojis_with_text(text)
  text.gsub(Unicode::Emoji::REGEX_WELL_FORMED_INCLUDE_TEXT) { 'XX' }
end

# Parse text and index emojis position and length in a hash
# example : {10 => 1, 35 => 5}
# means we have an emoji of length 1 at the index 10
# and we have an emoji of length 5 at the index 35
def index_emojis(text)
  emojis_indexes = {}

  text.to_enum(:scan, Unicode::Emoji::REGEX_WELL_FORMED_INCLUDE_TEXT).map do |m|
    m.length.times do |i|
      emojis_indexes[$`.size + i] = i.zero? ? m.length : 0
    end
  end

  emojis_indexes
end

Prawn::Text::Formatted::Arranger.class_eval do
  # This method repositions the x cursor after writing text fragments
  def fragment_measurements=(fragment)
    apply_font_settings(fragment) do
      fragment.width = @document.width_of(
        replace_emojis_with_text(fragment.text),
        kerning: @kerning
      )
      fragment.line_height = @document.font.height
      fragment.descender   = @document.font.descender
      fragment.ascender    = @document.font.ascender
    end
  end
end

Prawn::Text::Formatted::LineWrap.class_eval do
  # render char if line has enough space left
  def append_char(char)
    # kerning doesn't make sense in the context of a single character
    char_width = @document.width_of(replace_emojis_with_text(char))

    if @accumulated_width + char_width <= @width
      @accumulated_width += char_width
      @fragment_output << char
      true
    else
      false
    end
  end

  # parse segment and tries to render char
  def wrap_by_char(segment)
    emojis_indexes = index_emojis(segment)

    segment.each_char.with_index do |char, index|
      if emojis_indexes[index].present?
        # if length == 0, emoji has already been processed
        next if emojis_indexes[index].zero?

        char = segment[index, emojis_indexes[index]]
      end
      break unless append_char(char)
    end
  end

  # Regex to test if text contains more than one word
  def scan_pattern(encoding = ::Encoding::UTF_8)
    ebc = break_chars(encoding)
    eshy = soft_hyphen(encoding)
    ehy = hyphen(encoding)
    ews = whitespace(encoding)

    patterns = [
      "[^#{ebc}]+#{eshy}",
      "[^#{ebc}]+#{ehy}+",
      "[^#{ebc}]+",
      "[#{ews}]+",
      "#{ehy}+[^#{ebc}]*",
      eshy.to_s
    ]

    pattern = patterns
      .map { |p| p.encode(encoding) }
      .join('|')

    Regexp.new("#{Unicode::Emoji::REGEX_WELL_FORMED_INCLUDE_TEXT}|#{pattern}")
  end

  # This method calculates the line remaining space to decide when to start a new line
  def add_fragment_to_line(fragment)
    if fragment == ''
      true
    elsif fragment == "\n"
      @newline_encountered = true
      false
    else
      tokenize(fragment).each do |segment|
        segment_width = if segment == zero_width_space(segment.encoding)
                          0
                        else
                          @document.width_of(replace_emojis_with_text(segment), kerning: @kerning)
                        end

        if @accumulated_width + segment_width <= @width
          @accumulated_width += segment_width
          shy = soft_hyphen(segment.encoding)
          if segment[-1] == shy
            sh_width = @document.width_of(shy, kerning: @kerning)
            @accumulated_width -= sh_width
          end
          @fragment_output += segment
        else
          if @accumulated_width.zero? && @line_contains_more_than_one_word
            @line_contains_more_than_one_word = false
          end
          end_of_the_line_reached(segment)
          fragment_finished(fragment)
          return false
        end
      end

      fragment_finished(fragment)
      true
    end
  end
end

Prawn::Text::Formatted::Box.class_eval do
  # This methods parses the text and test that every char symbol is included in the current font
  # If a symbol is not included it is going to parse the provided fallback fonts
  # We needed to override this method so it does not break in half our Emojis
  # We also forced the font 'ipag' on the Emojis so they can be properly rendered in a text box
  def analyze_glyphs_for_fallback_font_support(hash)
    font_glyph_pairs = []
    original_font    = @document.font.family
    fragment_font    = hash[:font] || original_font
    fallback_fonts   = @fallback_fonts.dup

    # always default back to the current font if the glyph is missing from
    # all fonts
    fallback_fonts << fragment_font

    @document.save_font do
      emojis_indexes = index_emojis(hash[:text])

      # parse each char to test if we need to use a fallback font
      hash[:text].each_char.with_index do |char, index|
        if emojis_indexes[index].present?
          # if length == 0, emoji has already been processed
          next if emojis_indexes[index].zero?

          # Force japanese font ipag on emojis
          font_glyph_pairs << [
            'ipag', hash[:text][index, emojis_indexes[index]]
          ]
        else
          font_glyph_pairs << [
            find_font_for_this_glyph(
              char,
              fragment_font,
              fallback_fonts.dup
            ),
            char
          ]
        end
      end
    end

    # Don't add a :font to fragments if it wasn't there originally
    if hash[:font].nil?
      font_glyph_pairs.each do |pair|
        pair[0] = nil if pair[0] == original_font
      end
    end

    form_fragments_from_like_font_glyph_pairs(font_glyph_pairs, hash)
  end
end

Hoping this can help 🙏

@brandoncc
Copy link

Wow @ggmomo, that is awesome. I hope this moves the emoji conversation forward!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants