diff --git a/examples/custom_font/GenShinGothic-Normal.ttf b/examples/custom_font/GenShinGothic-Normal.ttf new file mode 100644 index 00000000..45a4b3f9 Binary files /dev/null and b/examples/custom_font/GenShinGothic-Normal.ttf differ diff --git a/examples/custom_font/custom_font.rb b/examples/custom_font/custom_font.rb new file mode 100644 index 00000000..464a700d --- /dev/null +++ b/examples/custom_font/custom_font.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +example :custom_font, 'Custom fonts' do |t| + Thinreports.configure do |config| + config.register_font('Gen Shin Gothic', + normal: t.resource('GenShinGothic-Normal.ttf') + ) + end + + text = <] @@ -32,5 +33,37 @@ def fallback_fonts def fallback_fonts=(font_names) @fallback_fonts = font_names.is_a?(Array) ? font_names : [font_names] end + + # @param [String] name + # @param [String] normal (required) Path for normal style font + # @param [String] bold Path for bold style font. Set :normal by default. + # @param [String] italic Path for italic style font. Set :normal by default. + # @param [String] bold_italic Path for bold+italic style font. Set :normal by default. + # @example + # config.register_font('Foo Font', + # normal: '/path/to/foo.ttf', + # bold: '/path/to/foo_bold.ttf', + # italic: '/path/to/foo_italic.ttf', + # bold_italic: '/path/to/foo_bold_italic.ttf' + # ) + # + # # For fonts that have no style, such as Japanese: + # config.register_font('Bar Font', normal: '/path/to/bar.tlf') + # => { + # normal: '/path/to/bar.tlf', + # bold: '/path/to/bar.tlf,' + # italic: '/path/to/bar.tlf,' + # bold_italic: '/path/to/bar.tlf,' + # } + def register_font(name, normal:, bold: normal, italic: normal, bold_italic: normal) + @fonts[name] = { + normal: normal, + bold: bold, + italic: italic, + bold_italic: bold_italic + } + end + + attr_reader :fonts end end diff --git a/lib/thinreports/core/errors.rb b/lib/thinreports/core/errors.rb index db121737..d52676a5 100644 --- a/lib/thinreports/core/errors.rb +++ b/lib/thinreports/core/errors.rb @@ -33,6 +33,13 @@ def initialize(id, item_type = 'Basic') end end + class UnknownFont < Basic + # @param [Prawn::Errors::UnknownFont] exception + def initialize(exception) + super("#{exception.class}: #{exception.message}") + end + end + class DisabledListSection < Basic def initialize(section) super("The #{section} section is disabled.") diff --git a/lib/thinreports/generator/pdf/document/font.rb b/lib/thinreports/generator/pdf/document/font.rb index 92229171..2ea2fcd7 100644 --- a/lib/thinreports/generator/pdf/document/font.rb +++ b/lib/thinreports/generator/pdf/document/font.rb @@ -23,9 +23,11 @@ module Font def setup_fonts # Install built-in fonts. BUILTIN_FONTS.each do |font_name, font_path| - install_font(font_name, font_path) + install_font(font_name, normal: font_path) end + install_custom_fonts + # Create aliases from the font list provided by Prawn. PRAWN_BUINTIN_FONT_ALIASES.each do |alias_name, name| pdf.font_families[alias_name] = pdf.font_families[name] @@ -37,7 +39,7 @@ def setup_fonts if pdf.font_families.key?(font) font else - install_font "Custom-fallback-font#{i}", font + install_font "Custom-fallback-font#{i}", normal: font end end @@ -46,20 +48,32 @@ def setup_fonts end # @param [String] name - # @param [String] file + # @param [String] normal (required) Path for normal style font + # @param [String] bold Path for bold style font. Set :normal by default. + # @param [String] italic Path for italic style font. Set :normal by default. + # @param [String] bold_italic Path for bold+italic style font. Set :normal by default. + # @raise [Thinreports::Errors::FontFileNotFound] # @return [String] installed font name - def install_font(name, file) - raise Errors::FontFileNotFound unless File.exist?(file) + def install_font(name, normal:, bold: normal, italic: normal, bold_italic: normal) + [normal, bold, italic, bold_italic].uniq.each do |font_file| + raise Thinreports::Errors::FontFileNotFound, font_file unless File.exist?(font_file) + end pdf.font_families[name] = { - normal: file, - bold: file, - italic: file, - bold_italic: file + normal: normal, + bold: bold, + italic: italic, + bold_italic: bold_italic } name end + def install_custom_fonts + Thinreports.config.fonts.each do |font_name, font_paths| + install_font(font_name, **font_paths) + end + end + # @return [String] def default_family 'Helvetica' diff --git a/lib/thinreports/generator/pdf/document/graphics/text.rb b/lib/thinreports/generator/pdf/document/graphics/text.rb index ef1e2ecd..2ab269d8 100644 --- a/lib/thinreports/generator/pdf/document/graphics/text.rb +++ b/lib/thinreports/generator/pdf/document/graphics/text.rb @@ -22,24 +22,27 @@ module Graphics # @option attrs [Boolean] :single (false) # @option attrs [:trancate, :shrink_to_fit, :expand] :overflow (:trancate) # @option attrs [:none, :break_word] :word_wrap (:none) + # @raise [Thinreports::Errors::UnknownFont] When :font can't be found in built-in fonts and registerd fonts def text_box(content, x, y, w, h, attrs = {}) - w, h = s2f(w, h) - box_attrs = text_box_attrs( - x, y, w, h, - single: attrs.delete(:single), - overflow: attrs[:overflow] - ) + return if attrs[:color] == 'none' + + # Building parameters for box + box_params = build_box_params(x, y, w, h, attrs) + # Building parameters for text + text_params = build_text_params(content, attrs) - # Do not break by word unless :word_wrap is :break_word - content = text_without_line_wrap(content) if attrs[:word_wrap] == :none + if need_bold_style_emulation?(text_params[:font], text_params[:styles]) + box_params[:mode] = :fill_stroke - with_text_styles(attrs) do |built_attrs, font_styles| - pdf.formatted_text_box( - [{ text: content, styles: font_styles }], - built_attrs.merge(box_attrs) - ) + emulate_bold_style(text_params[:color], text_params[:size]) do + pdf.formatted_text_box([text_params], box_params) + end + else + pdf.formatted_text_box([text_params], box_params) end + rescue Prawn::Errors::UnknownFont => e + raise Thinreports::Errors::UnknownFont, e rescue Prawn::Errors::CannotFit # Nothing to do. # @@ -54,66 +57,57 @@ def text(content, x, y, w, h, attrs = {}) text_box(content, x, y, w, h, { overflow: :shirink_to_fit }.merge(attrs)) end - private - - # @param x (see #text_box) - # @param y (see #text_box) - # @param w (see #text_box) - # @param h (see #text_box) - # @param [Hash] states - # @option states [Boolean] :single - # @option states [Symbold] :overflow - # @return [Hash] - def text_box_attrs(x, y, w, h, states = {}) - attrs = { - at: pos(x, y), - width: s2f(w) - } - if states[:single] - states[:overflow] != :expand ? attrs.merge(single_line: true) : attrs - else - attrs.merge(height: s2f(h)) - end - end + # @private + # + # @param (see #text_box) + # @return [Hash] Returns first parameter (Formatted Text Array) of Prawn::Document#formatted_text_box + # See http://prawnpdf.org/api-docs/2.0/Prawn/Text/Formatted.html + def build_box_params(x, y, w, h, attrs) + w, h = s2f(w, h) - # @param attrs (see #text) - # @yield [built_attrs, font_styles] - # @yieldparam [Hash] built_attrs The finalized attributes. - # @yieldparam [Array] font_styles The finalized styles. - def with_text_styles(attrs, &block) - # When no color is given, do not draw. - return unless attrs.key?(:color) && attrs[:color] != 'none' + {}.tap do |params| + params[:at] = pos(x, y) + params[:width] = w - save_graphics_state + [ + :align, + :valign, + :overflow + ].each { |param_key| params[param_key] = attrs[param_key] } - fontinfo = { - name: attrs.delete(:font).to_s, - color: parse_color(attrs.delete(:color)), - size: s2f(attrs.delete(:size)) - } - - # Add the specified value to :leading option. - line_height = attrs.delete(:line_height) - if line_height - attrs[:leading] = text_line_leading( - s2f(line_height), - name: fontinfo[:name], - size: fontinfo[:size] - ) - end + if attrs[:single] + params[:single_line] = attrs[:overflow] != :expand + else + params[:height] = h + end - # Set the :character_spacing option. - spacing = attrs.delete(:letter_spacing) - attrs[:character_spacing] = s2f(spacing) if spacing + if attrs[:line_height] + params[:leading] = text_line_leading(attrs[:line_height], name: attrs[:font], size: attrs[:size]) + end - # Or... with_font_styles(attrs, fontinfo, &block) - with_font_styles(attrs, fontinfo) do |modified_attrs, styles| - block.call(modified_attrs, styles) + if attrs[:letter_spacing] + params[:character_spacing] = attrs[:letter_spacing] + end end + end - restore_graphics_state + # @private + # + # @param (see #text_box) + # @return [Hash] Returns second parameter (Options) of Prawn::Document#formatted_text_box + # See http://prawnpdf.org/api-docs/2.0/Prawn/Text.html#text_box-instance_method + def build_text_params(content, attrs) + {}.tap do |params| + params[:text] = attrs[:word_wrap] == :none ? text_without_line_wrap(content) : content + params[:styles] = attrs[:styles] || [] + params[:size] = attrs[:size] + params[:font] = attrs[:font] + params[:color] = parse_color(attrs[:color]) + end end + # @private + # # @param [Numeric] line_height # @param [Hash] font # @option font [String] :name Name of font. @@ -123,49 +117,36 @@ def text_line_leading(line_height, font) line_height - pdf.font(font[:name], size: font[:size]).height end + # @private + # # @param [String] content # @return [String] def text_without_line_wrap(content) content.gsub(/ /, Prawn::Text::NBSP) end - # @param [Hash] attrs - # @param [Hash] font - # @option font [String] :color - # @option font [Numeric] :size - # @option font [String] :name - # @yield [attributes, styles] - # @yieldparam [Hash] modified_attrs - # @yieldparam [Array] styles - def with_font_styles(attrs, font, &block) - # Building font styles. - styles = attrs.delete(:styles) - - if styles - manual, styles = styles.partition do |style| - %i[bold italic].include?(style) && !font_has_style?(font[:name], style) - end - end + # @private + # + # @param [String] font_family + # @param [Array] font_styles + # @return [Boolean] + def need_bold_style_emulation?(font_family, font_styles) + font_styles.include?(:bold) && !font_has_style?(font_family, :bold) + end - # Emulate bold style. - if manual && manual.include?(:bold) - pdf.stroke_color(font[:color]) - pdf.line_width(font[:size] * 0.025) + # @private + # + # @param [String] font_color + # @param [Integer, Float] font_size + def emulate_bold_style(font_color, font_size, &block) + save_graphics_state - # Change rendering mode to :fill_stroke. - attrs[:mode] = :fill_stroke - end + pdf.stroke_color(font_color) + pdf.line_width(font_size * 0.025) - # Emulate italic style. - if manual && manual.include?(:italic) - # FIXME - # pdf.transformation_matrix(1, 0, 0.26, 1, 0, 0) - end + yield - pdf.font(font[:name], size: font[:size]) do - pdf.fill_color(font[:color]) - block.call(attrs, styles || []) - end + restore_graphics_state end end end diff --git a/test/unit/generator/pdf/document/test_font.rb b/test/unit/generator/pdf/document/test_font.rb index 5fda7faf..c3548504 100644 --- a/test/unit/generator/pdf/document/test_font.rb +++ b/test/unit/generator/pdf/document/test_font.rb @@ -32,7 +32,18 @@ def test_setup_fonts pdf.font_families[original_font] end - assert_equal Font::DEFAULT_FALLBACK_FONTS, %w[IPAMincho] + assert_equal %w[IPAMincho], pdf.fallback_fonts + end + + def test_setup_fonts_with_custom_fonts + Thinreports.config.register_font('Foo', normal: data_file('font.ttf')) + + assert_equal({ + normal: data_file('font.ttf'), + bold: data_file('font.ttf'), + italic: data_file('font.ttf'), + bold_italic: data_file('font.ttf') + }, document.pdf.font_families['Foo']) end def test_setup_fonts_with_custom_fallback_fonts diff --git a/test/unit/test_config.rb b/test/unit/test_config.rb index 861b0ed0..952739cf 100644 --- a/test/unit/test_config.rb +++ b/test/unit/test_config.rb @@ -33,4 +33,46 @@ def test_fallback_fonts config.fallback_fonts.unshift 'Times New Roman' assert_equal config.fallback_fonts, ['Times New Roman', 'Helvetica', 'IPAMincho'] end + + def test_register_font + config = Thinreports::Configuration.new + + assert_equal({}, config.fonts) + + config.register_font('Foo', + normal: 'foo.ttf', + bold: 'foo_bold.ttf', + italic: 'foo_italic.ttf', + bold_italic: 'foo_bold_italic.ttf' + ) + assert_equal({ + normal: 'foo.ttf', + bold: 'foo_bold.ttf', + italic: 'foo_italic.ttf', + bold_italic: 'foo_bold_italic.ttf' + }, config.fonts['Foo']) + + config.register_font('Bar', normal: 'bar.ttf') + assert_equal({ + normal: 'bar.ttf', + bold: 'bar.ttf', + italic: 'bar.ttf', + bold_italic: 'bar.ttf' + }, config.fonts['Bar']) + + assert_equal({ + 'Foo' => { + normal: 'foo.ttf', + bold: 'foo_bold.ttf', + italic: 'foo_italic.ttf', + bold_italic: 'foo_bold_italic.ttf' + }, + 'Bar' => { + normal: 'bar.ttf', + bold: 'bar.ttf', + italic: 'bar.ttf', + bold_italic: 'bar.ttf' + } + }, config.fonts) + end end