From a07a3860249e092e4d980f766ba9f594ff6b9ce5 Mon Sep 17 00:00:00 2001 From: Katsuya HIDAKA Date: Sun, 11 Mar 2018 18:08:54 +0900 Subject: [PATCH 1/7] Add configuration for registering font --- lib/thinreports/config.rb | 33 ++++++++++++++++++++++++++++++ test/unit/test_config.rb | 42 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/lib/thinreports/config.rb b/lib/thinreports/config.rb index 97983b45..4c160d95 100644 --- a/lib/thinreports/config.rb +++ b/lib/thinreports/config.rb @@ -15,6 +15,7 @@ def self.config class Configuration def initialize @fallback_fonts = [] + @fonts = {} end # @return [Array] @@ -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/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 From 438efd8669712869d310792ce4acb5a7fb66620e Mon Sep 17 00:00:00 2001 From: Katsuya HIDAKA Date: Wed, 14 Mar 2018 01:30:09 +0900 Subject: [PATCH 2/7] install custom fonts --- .../generator/pdf/document/font.rb | 32 +++++++++++++------ test/unit/generator/pdf/document/test_font.rb | 13 +++++++- 2 files changed, 35 insertions(+), 10 deletions(-) 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/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 From ed86a971daa57f9b117636684773ae92736cf8fc Mon Sep 17 00:00:00 2001 From: Katsuya HIDAKA Date: Wed, 14 Mar 2018 03:37:08 +0900 Subject: [PATCH 3/7] refactor text_box (WIP) --- .../generator/pdf/document/graphics/text.rb | 165 +++++++----------- 1 file changed, 60 insertions(+), 105 deletions(-) diff --git a/lib/thinreports/generator/pdf/document/graphics/text.rb b/lib/thinreports/generator/pdf/document/graphics/text.rb index ef1e2ecd..5788a215 100644 --- a/lib/thinreports/generator/pdf/document/graphics/text.rb +++ b/lib/thinreports/generator/pdf/document/graphics/text.rb @@ -25,21 +25,53 @@ module Graphics 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] - ) - - # Do not break by word unless :word_wrap is :break_word - content = text_without_line_wrap(content) if attrs[:word_wrap] == :none - - with_text_styles(attrs) do |built_attrs, font_styles| - pdf.formatted_text_box( - [{ text: content, styles: font_styles }], - built_attrs.merge(box_attrs) - ) + return if attrs[:color] == 'none' + + # Building parameters for box + box_params = {}.tap do |params| + params[:at] = pos(x, y) + params[:width] = w + + [ + :align, + :valign, + :overflow + ].each { |param_key| params[param_key] = attrs[param_key] } + + if attrs[:single] + params[:single_line] = attrs[:overflow] != :expand + else + params[:height] = h + end + + if attrs[:line_height] + params[:leading] = text_line_leading(attrs[:line_height], name: attrs[:font], size: attrs[:size]) + end + + if attrs[:letter_spacing] + params[:character_spacing] = attrs[:letter_spacing] + end end + + # Building parameters for text + text_params = {}.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 + + if need_bold_style_emulation?(text_params[:font], text_params[:styles]) + box_params[:mode] = :fill_stroke + + 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::CannotFit # Nothing to do. # @@ -56,64 +88,6 @@ def text(content, x, y, w, h, attrs = {}) 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 - - # @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' - - save_graphics_state - - 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 - - # Set the :character_spacing option. - spacing = attrs.delete(:letter_spacing) - attrs[:character_spacing] = s2f(spacing) if spacing - - # Or... with_font_styles(attrs, fontinfo, &block) - with_font_styles(attrs, fontinfo) do |modified_attrs, styles| - block.call(modified_attrs, styles) - end - - restore_graphics_state - end - # @param [Numeric] line_height # @param [Hash] font # @option font [String] :name Name of font. @@ -129,43 +103,24 @@ 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 + # @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) + # @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 From 59c12e5b36bcebd219b569e5d30fe862d188fdff Mon Sep 17 00:00:00 2001 From: Katsuya HIDAKA Date: Sun, 18 Mar 2018 13:59:50 +0900 Subject: [PATCH 4/7] refactor parameter builder --- .../generator/pdf/document/graphics/text.rb | 72 ++++++++++++------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/lib/thinreports/generator/pdf/document/graphics/text.rb b/lib/thinreports/generator/pdf/document/graphics/text.rb index 5788a215..351c00cf 100644 --- a/lib/thinreports/generator/pdf/document/graphics/text.rb +++ b/lib/thinreports/generator/pdf/document/graphics/text.rb @@ -23,12 +23,47 @@ module Graphics # @option attrs [:trancate, :shrink_to_fit, :expand] :overflow (:trancate) # @option attrs [:none, :break_word] :word_wrap (:none) def text_box(content, x, y, w, h, attrs = {}) - w, h = s2f(w, h) return if attrs[:color] == 'none' # Building parameters for box - box_params = {}.tap do |params| + box_params = build_box_params(x, y, w, h, attrs) + # Building parameters for text + text_params = build_text_params(content, attrs) + + if need_bold_style_emulation?(text_params[:font], text_params[:styles]) + box_params[:mode] = :fill_stroke + + 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::CannotFit + # Nothing to do. + # + # When the area is too small compared + # with the content and the style of the text. + # (See prawn/core/text/formatted/line_wrap.rb#L185) + end + + # @see #text_box + def text(content, x, y, w, h, attrs = {}) + # Set the :overflow property to :shirink_to_fit. + text_box(content, x, y, w, h, { overflow: :shirink_to_fit }.merge(attrs)) + 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) + + {}.tap do |params| params[:at] = pos(x, y) params[:width] = w @@ -52,38 +87,21 @@ def text_box(content, x, y, w, h, attrs = {}) params[:character_spacing] = attrs[:letter_spacing] end end + end - # Building parameters for text - text_params = {}.tap do |params| + # @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 - - if need_bold_style_emulation?(text_params[:font], text_params[:styles]) - box_params[:mode] = :fill_stroke - - 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::CannotFit - # Nothing to do. - # - # When the area is too small compared - # with the content and the style of the text. - # (See prawn/core/text/formatted/line_wrap.rb#L185) - end - - # @see #text_box - def text(content, x, y, w, h, attrs = {}) - # Set the :overflow property to :shirink_to_fit. - text_box(content, x, y, w, h, { overflow: :shirink_to_fit }.merge(attrs)) end private From c1486948b17db32e4dd49e591c4a6307522eec27 Mon Sep 17 00:00:00 2001 From: Katsuya HIDAKA Date: Sun, 18 Mar 2018 14:00:56 +0900 Subject: [PATCH 5/7] make public --- .../generator/pdf/document/graphics/text.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/thinreports/generator/pdf/document/graphics/text.rb b/lib/thinreports/generator/pdf/document/graphics/text.rb index 351c00cf..5bb2024f 100644 --- a/lib/thinreports/generator/pdf/document/graphics/text.rb +++ b/lib/thinreports/generator/pdf/document/graphics/text.rb @@ -104,8 +104,8 @@ def build_text_params(content, attrs) end end - private - + # @private + # # @param [Numeric] line_height # @param [Hash] font # @option font [String] :name Name of font. @@ -115,12 +115,16 @@ 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 + # @private + # # @param [String] font_family # @param [Array] font_styles # @return [Boolean] @@ -128,6 +132,8 @@ def need_bold_style_emulation?(font_family, font_styles) font_styles.include?(:bold) && !font_has_style?(font_family, :bold) end + # @private + # # @param [String] font_color # @param [Integer, Float] font_size def emulate_bold_style(font_color, font_size, &block) From a74f612dfaf302f0945d75e10ecd5054acf283b8 Mon Sep 17 00:00:00 2001 From: Katsuya HIDAKA Date: Sun, 18 Mar 2018 21:48:12 +0900 Subject: [PATCH 6/7] Thinreports raises Errors::UnknownFont when specified font family can't be found --- lib/thinreports/core/errors.rb | 7 +++++++ lib/thinreports/generator/pdf/document/graphics/text.rb | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) 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/graphics/text.rb b/lib/thinreports/generator/pdf/document/graphics/text.rb index 5bb2024f..2ab269d8 100644 --- a/lib/thinreports/generator/pdf/document/graphics/text.rb +++ b/lib/thinreports/generator/pdf/document/graphics/text.rb @@ -22,6 +22,7 @@ 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 = {}) return if attrs[:color] == 'none' @@ -40,7 +41,8 @@ def text_box(content, x, y, w, h, attrs = {}) 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. # From c37a5e3295f888b92314ddee6f6bc845803a2be5 Mon Sep 17 00:00:00 2001 From: Katsuya HIDAKA Date: Fri, 8 Jun 2018 18:02:40 +0900 Subject: [PATCH 7/7] Add example for CustomFont --- examples/custom_font/GenShinGothic-Normal.ttf | Bin 0 -> 4982252 bytes examples/custom_font/custom_font.rb | 27 + examples/custom_font/custom_font.tlf | 1072 +++++++++++++++++ 3 files changed, 1099 insertions(+) create mode 100644 examples/custom_font/GenShinGothic-Normal.ttf create mode 100644 examples/custom_font/custom_font.rb create mode 100644 examples/custom_font/custom_font.tlf diff --git a/examples/custom_font/GenShinGothic-Normal.ttf b/examples/custom_font/GenShinGothic-Normal.ttf new file mode 100644 index 0000000000000000000000000000000000000000..45a4b3f9cab793ae16ffa2fe73c873e468c77094 GIT binary patch literal 4982252 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