class HexaPDF::Layout::WidthFromPolygon
Utility class for generating width specifications for TextLayouter#fit
from polygons.
Public Class Methods
Creates a new object for the given polygon (or polygon set) and immediately prepares it so that call
can be used.
The offset argument specifies the vertical offset from the top at which calculations should start.
# File lib/hexapdf/layout/width_from_polygon.rb, line 47 def initialize(polygon, offset = 0) @polygon = polygon prepare(offset) end
Public Instance Methods
Returns the width specification for the given values with respect to the wrapped polygon.
# File lib/hexapdf/layout/width_from_polygon.rb, line 53 def call(height, line_height) width(@max_y - height - line_height, @max_y - height) end
Private Instance Methods
Prepare the segments and other data for later use.
# File lib/hexapdf/layout/width_from_polygon.rb, line 199 def prepare(offset) @max_y = @polygon.bbox.max_y - offset @polygon_segments = if @polygon.nr_of_contours > 1 @polygon.polygons.map {|polygon| process_polygon(polygon) } else [process_polygon(@polygon)] end end
Processes the given polygon segment by segment and returns an array with the following processing information for each segment of the polygon:
-
the segment itself
-
minimum y-value
-
maximum y-value
-
x-value corresponding to the minimum y-value
-
x-value corresponding to the maximum y-value
-
whether the segment is vertical
-
for non-vertical segments: slope and y-intercept of the segment
Additionally, the returned array is rotated sothat the data for the segment with the minimum x-value is the first item (without changing the order).
# File lib/hexapdf/layout/width_from_polygon.rb, line 221 def process_polygon(polygon) rotate_nr = 0 min_x = Float::INFINITY segments = polygon.each_segment.reject(&:horizontal?) segments.map!.with_index do |segment, index| (rotate_nr = index; min_x = segment.min.x) if segment.min.x < min_x data = [segment] if segment.start_point.y < segment.end_point.y data.push(segment.start_point.y, segment.end_point.y, segment.start_point.x, segment.end_point.x) else data.push(segment.end_point.y, segment.start_point.y, segment.end_point.x, segment.start_point.x) end data.push(segment.vertical?) unless segment.vertical? data.push(segment.slope) data.push((segment.start_point.y - segment.slope * segment.start_point.x).to_f) end data end segments.rotate!(rotate_nr) end
Calculates the width specification for the area between the horizontal lines at y1 < y2.
The following algorithm is used: Given y1 < y2 as the horizontal lines between which text should be layed out, and a polygon set p that is not self-intersecting but may have arbitrarily nested holes:
-
Get all segments of the polygon set in sequence, removing the horizontal segments in the process (done in
prepare
). -
Make sure that the first segment represents a left-most outside-inside transition, rotate array of segments (separate for each polygon) if necessary. (done in
prepare
) -
For the segments of each polygon do separately:
-
Ignore all segments except those with min_y < y2 and max_y > y1.
-
Determine the min_x and max_x of the segment within y1 <= y2.
-
If the segment crosses both, y1 and y2, store min_x/max_x and this segment is finished. Otherwise traverse the segments in-order to find the next crossing, updating min_x/max_x in the process. If it crosses the other line, the result is the same as if a single segment had crossed both lines. Otherwise the result depends on whether the segment sequence represents an outside-inside transition (it is ignored) or inside-outside transition (store two pairs min_x/min_x and max_x/max_x).
-
-
Order stored x-values.
-
For each pair [a_min, a_max], [b_min, b_max]
-
if inside (index is even): calculate width = b_min - a_max
-
if outside: calculate offset = b_max - a_min
-
-
Prepend a0_max for first offset and remove all offset-width pairs where width is zero.
# File lib/hexapdf/layout/width_from_polygon.rb, line 91 def width(y1, y2) result = [] @polygon_segments.each do |segments| temp_result = [] status = if segments.first[0].start_point.y > y2 || segments.first[0].start_point.y < y1 :outside else :inside end segments.each do |_segment, miny, maxy, minyx, maxyx, vertical, slope, intercept| next unless miny < y2 && maxy > y1 if vertical min_x = max_x = minyx else min_x = (miny <= y1 ? (y1 - intercept) / slope : (miny <= y2 ? minyx : maxyx)) max_x = (maxy >= y2 ? (y2 - intercept) / slope : (miny >= y1 ? minyx : maxyx)) min_x, max_x = max_x, min_x if min_x > max_x end if miny <= y1 && maxy >= y2 # segment crosses both lines temp_result << [min_x, max_x, :crossed_both] elsif miny <= y1 # segment crosses bottom line if status == :outside temp_result << [min_x, max_x, :crossed_bottom] status = :inside elsif temp_result.last temp_result.last[0] = min_x if temp_result.last[0] > min_x temp_result.last[1] = max_x if temp_result.last[1] < max_x temp_result.last[2] = :crossed_both if temp_result.last[2] == :crossed_top temp_result.last[2] = :crossed_bottom if temp_result.last[2] == :crossed_none status = :outside else temp_result << [min_x, max_x, :crossed_bottom] status = :outside end elsif maxy >= y2 # segment crosses top line if status == :outside temp_result << [min_x, max_x, :crossed_top] status = :inside elsif temp_result.last temp_result.last[0] = min_x if temp_result.last[0] > min_x temp_result.last[1] = max_x if temp_result.last[1] < max_x temp_result.last[2] = :crossed_both if temp_result.last[2] == :crossed_bottom temp_result.last[2] = :crossed_top if temp_result.last[2] == :crossed_none status = :outside else temp_result << [min_x, max_x, :crossed_top] status = :outside end elsif status == :inside && temp_result.last # segment crosses no line temp_result.last[0] = min_x if temp_result.last[0] > min_x temp_result.last[1] = max_x if temp_result.last[1] < max_x else # first segment completely inside temp_result << [min_x, max_x, :crossed_none] end end if temp_result.empty? # Ignore degenerate results next elsif temp_result.size == 1 # either polygon completely inside or just the top/bottom part, handle the same temp_result[0][2] = :crossed_top elsif temp_result[0][2] != :crossed_both && temp_result[-1][2] != :crossed_both # Handle case where first and last segments only crosses one line temp_result[0][0] = temp_result[-1][0] if temp_result[0][0] > temp_result[-1][0] temp_result[0][1] = temp_result[-1][1] if temp_result[0][1] < temp_result[-1][1] temp_result[0][2] = :crossed_both if temp_result[0][2] != temp_result[-1][2] temp_result.pop end result.concat(temp_result) end temp_result = result outside = true temp_result.sort_by! {|a| a[0] }.map! do |min, max, stat| if stat == :crossed_both outside = !outside [min, max] elsif outside [] else [min, min, max, max] end end.flatten! temp_result.unshift(0, 0) i = 0 result = [] while i < temp_result.size - 2 if i % 4 == 2 # inside the polygon, i.e. width (min2 - max1) if (width = temp_result[i + 2] - temp_result[i + 1]) > 0 result << width else result.pop # remove last offset and don't add width end else # outside the polygon, i.e. offset (max2 - min1) result << temp_result[i + 3] - temp_result[i + 0] end i += 2 end result.empty? ? [0, 0] : result end