Module: Idml::TextEngine::CjkLayout
- Defined in:
- lib/idml/text_engine/cjk_layout.rb
Overview
CJK text layout utilities: character classification, kinsoku shori (line-breaking rules), and vertical writing-mode helpers.
Constant Summary collapse
- SCRIPT_SPACING_EM =
Mojikumi subset: automatic inter-script spacing — an eighth em is inserted between adjacent CJK and ASCII alphanumeric glyphs (widenning the leading glyph of each pair), matching InDesign's default CJK/Latin auto spacing. A named mojikumi set's OverrideMojikumiAki entries take precedence when given: the Desired value (em units) applies to each matching class pair (TODO 145).
0.125
- CLASS_IDEOGRAPH =
Mojikumi character classes (InDesign mojikumi table rows).
1- CLASS_OPENING =
2- CLASS_CLOSING =
3- CLASS_COMMA_PERIOD =
4- CLASS_MIDDLE_DOT =
5- CLASS_DIGIT =
6- CLASS_LATIN =
7- COMMA_PERIOD_CODEPOINTS =
[ 0x3001, # 、 ideographic comma 0x3002, # 。 ideographic full stop ].freeze
- CLOSING_CODEPOINTS =
[ 0x3009, # 〉 0x300B, # 》 0x300D, # 」 0x300F, # 』 0x3011, # 】 0x3015, # 〕 0xFF09, # )fullwidth right paren 0xFF3D, # ] 0xFF5D, # } ].freeze
- TRAILING_COMPRESSION_CODEPOINTS =
Mojikumi subset 2: line-end punctuation compression — full-width closing punctuation occupying a line's final position renders at half advance (行末約物半角詰め), tightening the line as InDesign's default mojikumi does.
[ 0x3001, # 、 ideographic comma 0x3002, # 。 ideographic full stop 0x3009, # 〉 0x300B, # 》 0x300D, # 」 0x300F, # 』 0x3011, # 】 0x3015, # 〕 0xFF09, # )fullwidth right paren 0xFF3D, # ] 0xFF5D, # } 0x30FC, # ー prolonged sound mark ].freeze
- OPENING_CODEPOINTS =
Class-based mojikumi pair compression: a full-width closing/middle glyph followed by another full-width punctuation glyph compresses to half advance within the line (。」、ー) … pairs), matching InDesign's default mojikumi behavior for consecutive 約物.
[ 0x300C, # 「 0x300E, # 『 0x3008, # 〈 0x300A, # 《 0x3010, # 【 0x3014, # 〔 0xFF08, # (fullwidth left paren 0xFF3B, # [ 0xFF5B, # { ].freeze
- MIDDLE_CODEPOINTS =
[ 0x30FB, # ・ fullwidth middle dot 0x30FC, # ー prolonged sound mark 0x2025, # ‥ two-dot leader 0x2026, # … ellipsis ].freeze
- CJK_RANGES =
[ 0x3000..0x303F, # CJK Symbols and Punctuation 0x3040..0x309F, # Hiragana 0x30A0..0x30FF, # Katakana 0x3400..0x4DBF, # CJK Extension A 0x4E00..0x9FFF, # CJK Unified Ideographs 0xAC00..0xD7AF, # Hangul Syllables 0xF900..0xFAFF, # CJK Compatibility Ideographs 0xFF00..0xFFEF, # Halfwidth and Fullwidth Forms ].freeze
- FORBIDDEN_LINE_START =
Characters forbidden at line start (kinsoku shori).
%w[ 、 。, . 。 : ; ? ソ ゙ ゚ ! ! ” 〉 》 」 』 】 〕 } ) 」 ゛ ゝ ゞ 〃 々 〆 〇 ー 〜 ぁ ぃ ぅ ぇ ぉ っ ゃ ゅ ょ ゎ ァ ィ ゥ ェ ォ ッ ャ ュ ョ ヮ ぁ ぃ ぅ ぇ ぉ ].map(&:codepoints).flatten.freeze
- FORBIDDEN_LINE_END =
Characters forbidden at line end (kinsoku shori).
%w[ “ 〈 《 「 『 【 〔 { ( 「 ].map(&:codepoints).flatten.freeze
Class Method Summary collapse
-
.adjust_boundary(prev_line, current_line) ⇒ Object
Adjust the boundary between two lines according to kinsoku rules.
-
.aki_em(left, right, aki_overrides) ⇒ Object
Spacing (em units) between an adjacent pair: the named set's Desired aki for a matching class-pair override, else the default eighth-em at CJK/Latin script boundaries, else none.
-
.apply_kinsoku(lines) ⇒ Object
Apply kinsoku shori to an array of Line objects.
- .apply_line_end_compression(lines) ⇒ Object
- .apply_pair_compression(lines) ⇒ Object
- .apply_script_spacing(glyphs, size, aki_overrides = []) ⇒ Object
- .ascii_alnum?(codepoint) ⇒ Boolean
- .cjk?(codepoint) ⇒ Boolean
- .closing?(codepoint) ⇒ Boolean
- .compressible?(codepoint) ⇒ Boolean
-
.contains_cjk?(text) ⇒ Boolean
Check if text contains any CJK characters.
- .digit_class?(codepoint) ⇒ Boolean
- .dup_line(line) ⇒ Object
- .forbidden_end?(codepoint) ⇒ Boolean
- .forbidden_start?(codepoint) ⇒ Boolean
-
.fullwidth_digit?(codepoint) ⇒ Boolean
Detect if a codepoint is a CJK full-width digit (0-9).
- .fullwidth_punct?(codepoint) ⇒ Boolean
- .latin_class?(codepoint) ⇒ Boolean
- .middle?(codepoint) ⇒ Boolean
-
.mojikumi_class(codepoint) ⇒ Object
The mojikumi class of a codepoint, or nil outside the CJK punctuation / script classes.
-
.pair_compressible?(leading, following) ⇒ Boolean
Closing/middle glyph followed by full-width punctuation compresses the leading glyph to half advance.
-
.script_boundary?(left, right) ⇒ Boolean
True when exactly one of the pair is CJK and the other is an ASCII letter or digit.
-
.tate_chu_yoko?(codepoint) ⇒ Boolean
Tate-Chu-Yoko candidate: 1-2 digit horizontal runs in vertical text.
-
.vertical_mode?(orientation) ⇒ Boolean
Check if a StoryOrientation value indicates vertical writing.
Class Method Details
.adjust_boundary(prev_line, current_line) ⇒ Object
Adjust the boundary between two lines according to kinsoku rules.
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 277 def adjust_boundary(prev_line, current_line) prev = dup_line(prev_line) curr = dup_line(current_line) # Move forbidden-start chars from current line start to prev line end. until curr.glyphs.empty? || !forbidden_start?(curr.glyphs.first.codepoint) moved = curr.glyphs.shift prev.glyphs << moved prev.width += moved.width curr.width -= moved.width end # Move forbidden-end chars from prev line end to current line start. until prev.glyphs.empty? || !forbidden_end?(prev.glyphs.last.codepoint) moved = prev.glyphs.pop prev.width -= moved.width curr.glyphs.unshift(moved) curr.width += moved.width end [prev, curr] end |
.aki_em(left, right, aki_overrides) ⇒ Object
Spacing (em units) between an adjacent pair: the named set's Desired aki for a matching class-pair override, else the default eighth-em at CJK/Latin script boundaries, else none.
62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 62 def aki_em(left, right, aki_overrides) override = aki_overrides.find do |entry| entry.side_is_after_target && entry.target_mojikumi_class == mojikumi_class(left) && entry.side_mojikumi_class == mojikumi_class(right) end return override.desired if override&.desired return SCRIPT_SPACING_EM if script_boundary?(left, right) nil end |
.apply_kinsoku(lines) ⇒ Object
Apply kinsoku shori to an array of Line objects. Moves forbidden-start chars from the beginning of a line to the end of the previous line, and forbidden-end chars from the end of a line to the beginning of the next line.
258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 258 def apply_kinsoku(lines) return lines if lines.length < 2 result = [dup_line(lines.first)] lines[1..].each do |line| prev = result.last curr = dup_line(line) adjusted_prev, adjusted_current = adjust_boundary(prev, curr) result[-1] = adjusted_prev result << adjusted_current end result end |
.apply_line_end_compression(lines) ⇒ Object
134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 134 def apply_line_end_compression(lines) lines.each do |line| last = line.glyphs.last next unless last next unless compressible?(last.codepoint) last.width /= 2.0 line.width = line.glyphs.sum(&:width) end lines end |
.apply_pair_compression(lines) ⇒ Object
174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 174 def apply_pair_compression(lines) lines.each do |line| glyphs = line.glyphs (0...(glyphs.length - 1)).each do |index| leading = glyphs[index] following = glyphs[index + 1] next unless pair_compressible?(leading, following) leading.width /= 2.0 end line.width = glyphs.sum(&:width) end lines end |
.apply_script_spacing(glyphs, size, aki_overrides = []) ⇒ Object
45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 45 def apply_script_spacing(glyphs, size, aki_overrides = []) (0...(glyphs.length - 1)).each do |index| leading = glyphs[index] following = glyphs[index + 1] em = aki_em(leading.codepoint, following.codepoint, aki_overrides) next unless em&.positive? leading.width += em * size end glyphs end |
.ascii_alnum?(codepoint) ⇒ Boolean
109 110 111 112 113 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 109 def ascii_alnum?(codepoint) (0x30..0x39).cover?(codepoint) || (0x41..0x5A).cover?(codepoint) || (0x61..0x7A).cover?(codepoint) end |
.cjk?(codepoint) ⇒ Boolean
237 238 239 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 237 def cjk?(codepoint) CJK_RANGES.any? { |range| range.include?(codepoint) } end |
.closing?(codepoint) ⇒ Boolean
199 200 201 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 199 def closing?(codepoint) TRAILING_COMPRESSION_CODEPOINTS.include?(codepoint) end |
.compressible?(codepoint) ⇒ Boolean
146 147 148 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 146 def compressible?(codepoint) TRAILING_COMPRESSION_CODEPOINTS.include?(codepoint) end |
.contains_cjk?(text) ⇒ Boolean
Check if text contains any CJK characters.
250 251 252 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 250 def contains_cjk?(text) text.each_codepoint.any? { |cp| cjk?(cp) } end |
.digit_class?(codepoint) ⇒ Boolean
90 91 92 93 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 90 def digit_class?(codepoint) (0x30..0x39).cover?(codepoint) || (0xFF10..0xFF19).cover?(codepoint) end |
.dup_line(line) ⇒ Object
272 273 274 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 272 def dup_line(line) Line.new(line.glyphs.dup, line.width, line.x_offset) end |
.forbidden_end?(codepoint) ⇒ Boolean
245 246 247 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 245 def forbidden_end?(codepoint) FORBIDDEN_LINE_END.include?(codepoint) end |
.forbidden_start?(codepoint) ⇒ Boolean
241 242 243 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 241 def forbidden_start?(codepoint) FORBIDDEN_LINE_START.include?(codepoint) end |
.fullwidth_digit?(codepoint) ⇒ Boolean
Detect if a codepoint is a CJK full-width digit (0-9).
301 302 303 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 301 def fullwidth_digit?(codepoint) codepoint.between?(0xFF10, 0xFF19) end |
.fullwidth_punct?(codepoint) ⇒ Boolean
207 208 209 210 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 207 def fullwidth_punct?(codepoint) closing?(codepoint) || middle?(codepoint) || OPENING_CODEPOINTS.include?(codepoint) end |
.latin_class?(codepoint) ⇒ Boolean
95 96 97 98 99 100 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 95 def latin_class?(codepoint) (0x41..0x5A).cover?(codepoint) || (0x61..0x7A).cover?(codepoint) || (0xFF21..0xFF3A).cover?(codepoint) || (0xFF41..0xFF5A).cover?(codepoint) end |
.middle?(codepoint) ⇒ Boolean
203 204 205 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 203 def middle?(codepoint) MIDDLE_CODEPOINTS.include?(codepoint) end |
.mojikumi_class(codepoint) ⇒ Object
The mojikumi class of a codepoint, or nil outside the CJK punctuation / script classes. Ideographs (the default CJK class) win only when no special class matches.
78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 78 def mojikumi_class(codepoint) return CLASS_OPENING if OPENING_CODEPOINTS.include?(codepoint) return CLASS_COMMA_PERIOD if COMMA_PERIOD_CODEPOINTS.include?(codepoint) return CLASS_CLOSING if CLOSING_CODEPOINTS.include?(codepoint) return CLASS_MIDDLE_DOT if MIDDLE_CODEPOINTS.include?(codepoint) return CLASS_DIGIT if digit_class?(codepoint) return CLASS_LATIN if latin_class?(codepoint) return CLASS_IDEOGRAPH if cjk?(codepoint) nil end |
.pair_compressible?(leading, following) ⇒ Boolean
Closing/middle glyph followed by full-width punctuation compresses the leading glyph to half advance.
191 192 193 194 195 196 197 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 191 def pair_compressible?(leading, following) return false unless fullwidth_punct?(following.codepoint) closing?(leading.codepoint) || (middle?(leading.codepoint) && !OPENING_CODEPOINTS.include?(following.codepoint)) end |
.script_boundary?(left, right) ⇒ Boolean
True when exactly one of the pair is CJK and the other is an ASCII letter or digit.
104 105 106 107 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 104 def script_boundary?(left, right) (cjk?(left) && ascii_alnum?(right)) || (ascii_alnum?(left) && cjk?(right)) end |
.tate_chu_yoko?(codepoint) ⇒ Boolean
Tate-Chu-Yoko candidate: 1-2 digit horizontal runs in vertical text.
306 307 308 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 306 def tate_chu_yoko?(codepoint) codepoint.between?(0x30, 0x39) || fullwidth_digit?(codepoint) end |
.vertical_mode?(orientation) ⇒ Boolean
Check if a StoryOrientation value indicates vertical writing.
311 312 313 |
# File 'lib/idml/text_engine/cjk_layout.rb', line 311 def vertical_mode?(orientation) ["TopToBottom", "RightToLeftTopToBottom"].include?(orientation) end |