Skip to content

Commit

Permalink
resolves asciidoctor#155 support CMYK color values
Browse files Browse the repository at this point in the history
- parse CMYK color values in theme
- parse CMYK color values in formatted text
- avoid redundant processing of color values when loading theme
- mixin color types into color values
- show example of CMYK value in default theme
- code formatting
  • Loading branch information
mojavelinux committed May 27, 2015
1 parent 578eabd commit 2a5f9a6
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 45 deletions.
6 changes: 6 additions & 0 deletions data/themes/default-theme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ page:
# size can be a named size (e.g., A4) or custom dimensions (e.g., [8.25in, 11.69in])
size: Letter
base:
# color as hex string (leading # is optional)
font_color: 333333
# color as RGB array
#font_color: [51, 51, 51]
# color as CMYK array (approximated)
#font_color: [0, 0, 0, 0.92]
#font_color: [0, 0, 0, 92%]
font_family: NotoSerif
# choose one of these font_size/line_height_length combinations
#font_size: 14
Expand Down
4 changes: 4 additions & 0 deletions lib/asciidoctor-pdf/converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,7 @@ def convert_inline_button node

def convert_inline_callout node
if (conum_color = @theme.conum_font_color)
# NOTE CMYK value gets flattened here, but is restored by formatted text parser
%(<color rgb="#{conum_color}">#{conum_glyph node.text.to_i}</color>)
else
node.text
Expand All @@ -1196,6 +1197,7 @@ def convert_inline_footnote node
#text = node.document.footnotes.find {|fn| fn.index == index }.text
%( [#{node.text}])
elsif node.type == :xref
# NOTE footnote reference not found
%( <color rgb="FF0000">[#{node.text}]</color>)
end
end
Expand Down Expand Up @@ -1437,6 +1439,7 @@ def layout_prose string, opts = {}
if (anchor = opts.delete :anchor)
# FIXME won't work if inline_format is true; should instead pass through as attribute w/ link color set
if (link_color = opts.delete :link_color)
# NOTE CMYK value gets flattened here, but is restored by formatted text parser
string = %(<a anchor="#{anchor}"><color rgb="#{link_color}">#{string}</color></a>)
else
string = %(<a anchor="#{anchor}">#{string}</a>)
Expand Down Expand Up @@ -1522,6 +1525,7 @@ def layout_toc_level sections, num_levels, line_metrics, dot_width, num_front_ma
# NOTE we do some cursor hacking here so the dots don't affect vertical alignment
start_page_number = page_number
start_cursor = cursor
# NOTE CMYK value gets flattened here, but is restored by formatted text parser
typeset_text %(<a anchor="#{sect_anchor = (sect.attr 'anchor') || sect.id}"><color rgb="#{toc_font_color}">#{sect_title}</color></a>), line_metrics, inline_format: true
# we only write the label if this is a dry run
unless scratch?
Expand Down
4 changes: 2 additions & 2 deletions lib/asciidoctor-pdf/prawn_ext/extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,8 @@ def fill_absolute_bounds f_color = fill_color
# color, stroke color and line width on the document are restored.
#
def fill_and_stroke_bounds f_color = fill_color, s_color = stroke_color, options = {}
no_fill = (f_color.nil? || f_color.to_s == 'transparent')
no_stroke = (s_color.nil? || s_color.to_s == 'transparent')
no_fill = !f_color || f_color == 'transparent'
no_stroke = !s_color || s_color == 'transparent'
return if no_fill && no_stroke
save_graphics_state do
radius = options[:radius] || 0
Expand Down
22 changes: 18 additions & 4 deletions lib/asciidoctor-pdf/prawn_ext/formatted_text/transform.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def apply(parsed)
previous_fragment_is_text = false
end
when :text, :entity
# TODO could avoid this redundant type check by splitting :text and :entity cases
node_text = if node_type == :text
node[:value]
elsif node_type == :entity
Expand Down Expand Up @@ -111,12 +112,25 @@ def build_fragment(fragment, tag_name = nil, attrs = {})
when :color
if !fragment[:color]
if (rgb = attrs[:rgb])
if rgb[0] == '#'
rgb = rgb[1..-1]
case rgb.chr
when '#'
fragment[:color] = rgb[1..-1]
when '['
# treat value as CMYK array (e.g., "[50, 100, 0, 0]")
fragment[:color] = rgb[1..-1].chomp(']').split(', ').map(&:to_i)
# ...or we could honor an rgb array too
#case (vals = rgb[1..-1].chomp(']').split(', ')).size
#when 4
# fragment[:color] = vals.map(&:to_i)
#when 3
# fragment[:color] = vals.map {|e| '%02X' % e.to_i }.join
#end
else
fragment[:color] = rgb
end
fragment[:color] = rgb
# QUESTION should we even support r,g,b and c,m,y,k as individual values?
elsif (r = attrs[:r]) && (g = attrs[:g]) && (b = attrs[:b])
fragment[:color] = [r, g, b].map {|e| '%02x' % e.to_i }.join
fragment[:color] = [r, g, b].map {|e| '%02X' % e.to_i }.join
elsif (c = attrs[:c]) && (m = attrs[:m]) && (y = attrs[:y]) && (k = attrs[:k])
fragment[:color] = [c.to_i, m.to_i, y.to_i, k.to_i]
end
Expand Down
126 changes: 87 additions & 39 deletions lib/asciidoctor-pdf/theme_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,45 @@
module Asciidoctor
module Pdf
class ThemeLoader
DataDir = ::File.expand_path ::File.join(::File.dirname(__FILE__), '..', '..', 'data')
ThemesDir = ::File.join DataDir, 'themes'
FontsDir = ::File.join DataDir, 'fonts'
DataDir = ::File.expand_path(::File.join(::File.dirname(__FILE__), '..', '..', 'data'))
ThemesDir = ::File.join(DataDir, 'themes')
FontsDir = ::File.join(DataDir, 'fonts')
HexColorValueRx = /_color: (?<quote>"|'|)#?(?<value>[A-Za-z0-9]{3,6})\k<quote>$/

module ColorValue; end

class HexColorValue < String
include ColorValue
end

# A marker module for a normalized CMYK array
# Prevents normalizing CMYK value more than once
module CmykColorValue
include ColorValue
def to_s
%([#{join ', '}])
end
end

def self.resolve_theme_file theme_name = nil, theme_path = nil
theme_name ||= 'default'
# if .yml extension is given, assume it's a full file name
if theme_name.end_with? '.yml'
if theme_name.end_with?('.yml')
# FIXME restrict to jail!
# QUESTION why are we not using expand_path in this case?
theme_path ? (::File.join theme_path, theme_name) : theme_name
theme_path ? ::File.join(theme_path, theme_name) : theme_name
else
# QUESTION should we append '-theme.yml' or just '.yml'?
::File.expand_path %(#{theme_name}-theme.yml), (theme_path || ThemesDir)
::File.expand_path(%(#{theme_name}-theme.yml), (theme_path || ThemesDir))
end
end

def self.resolve_theme_asset asset_path, theme_path = nil
::File.expand_path asset_path, (theme_path || ThemesDir)
::File.expand_path(asset_path, (theme_path || ThemesDir))
end

def self.load_theme theme_name = nil, theme_path = nil
load_file (resolve_theme_file theme_name, theme_path)
load_file(resolve_theme_file(theme_name, theme_path))
end

def self.load_file filename
Expand All @@ -37,15 +52,15 @@ def self.load_file filename
end

def load hash
hash.inject(::OpenStruct.new) do |s, (k, v)|
if v.kind_of? ::Hash
v.each do |k2, v2|
s[%(#{k}_#{k2})] = (k2.end_with? '_color') ? to_hex(evaluate(v2, s)) : evaluate(v2, s)
hash.inject(::OpenStruct.new) do |data, (key, val)|
if ::Hash === val
val.each do |key2, val2|
data[%(#{key}_#{key2})] = key2.end_with?('_color') ? to_color(evaluate(val2, data)) : evaluate(val2, data)
end
else
s[k] = (k.end_with? '_color') ? to_hex(evaluate(v, s)) : evaluate(v, s)
data[key] = key.end_with?('_color') ? to_color(evaluate(val, data)) : evaluate(val, data)
end
s
data
end
end

Expand All @@ -62,20 +77,18 @@ def evaluate expr, vars
end
end

# NOTE assume expr is a String
def expand_vars expr, vars
if expr.include? '$'
if (expr.start_with? '$') && (expr.match /^\$([a-z0-9_]+)$/)
vars[$1]
else
expr.gsub(/\$([a-z0-9_]+)/) { vars[$1] }
end
return expr unless expr.include? '$'
if expr.start_with?('$') && expr.match(/^\$([a-z0-9_]+)$/)
vars[$1]
else
expr
expr.gsub(/\$([a-z0-9_]+)/) { vars[$1] }
end
end

def evaluate_math expr
return expr unless ::String === expr
return expr if !(::String === expr) || ColorValue === expr
original = expr
# FIXME quick HACK to turn a single negative number into an expression
expr = %(1 - #{expr[1..-1]}) if expr.start_with?('-')
Expand Down Expand Up @@ -108,35 +121,70 @@ def evaluate_math expr
expr = result
break if unchanged
end
if (expr.end_with? ')') && (expr.match /^(round|floor|ceil)\(/)
if expr.end_with?(')') && expr.match(/^(round|floor|ceil)\(/)
op = $1
offset = op.length + 1
expr = expr[offset...-1].to_f.send(op.to_sym)
end
if original == expr
expr
if expr == original
original
else
if ((int_val = expr.to_i) == (float_val = expr.to_f))
int_val
else
float_val
end
(int_val = expr.to_i) == (flt_val = expr.to_f) ? int_val : flt_val
end
end

def to_hex value
str = value.to_s
return str if str == 'transparent'
hex = case str.size
def to_color value
case value
when ColorValue
# already converted
return value
when ::String
if value == 'transparent'
# FIXME should we have a TransparentColorValue class?
return HexColorValue.new(value)
elsif value.size == 6
return HexColorValue.new(value.upcase)
end
when ::Array
case value.size
# CMYK value
when 4
value = value.map {|e|
if ::Numeric === e
e = e * 100.0 unless e > 1
else
e = e.to_s.chomp('%').to_f
end
(e == (int_e = e.to_i)) ? int_e : e
}
if value == [0, 0, 0, 0]
return HexColorValue.new('FFFFFF')
else
value.extend CmykColorValue
return value
end
# RGB value
when 3
return HexColorValue.new(value.map {|e| '%02X' % e}.join)
# Nonsense array value; flatten to string
else
value = value.join
end
else
# Unknown type; coerce to a string
value = %(#{value})
end
value = case value.size
when 6
str
value
when 3
str.each_char.map {|it| it * 2 }.join
# expand hex shorthand (e.g., f00 -> ff0000)
value.each_char.map {|c| c * 2 }.join
else
# CAUTION: YAML will misinterpret values with leading zeros (e.g., 000011) that are not quoted (aside from 000000)
str[0..5].rjust(6, '0')
# truncate or pad with leading zeros (e.g., ff -> 0000ff)
value[0..5].rjust 6, '0'
end
hex.upcase
HexColorValue.new(value.upcase)
end
end
end
Expand Down

0 comments on commit 2a5f9a6

Please sign in to comment.