# -*- encoding: utf-8; frozen_string_literal: true -*-
#
#--
# This file is part of HexaPDF.
#
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
# Copyright (C) 2014-2019 Thomas Leitner
#
# HexaPDF is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License version 3 as
# published by the Free Software Foundation with the addition of the
# following permission added to Section 15 as permitted in Section 7(a):
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
# INFRINGEMENT OF THIRD PARTY RIGHTS.
#
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with HexaPDF. If not, see .
#
# The interactive user interfaces in modified source and object code
# versions of HexaPDF must display Appropriate Legal Notices, as required
# under Section 5 of the GNU Affero General Public License version 3.
#
# In accordance with Section 7(b) of the GNU Affero General Public
# License, a covered work must retain the producer line in every PDF that
# is created or manipulated using HexaPDF.
#
# If the GNU Affero General Public License doesn't fit your need,
# commercial licenses are available at .
#++
module HexaPDF
module Layout
# Utility class for generating width specifications for TextLayouter#fit from polygons.
class WidthFromPolygon
# 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.
def initialize(polygon, offset = 0)
@polygon = polygon
prepare(offset)
end
# Returns the width specification for the given values with respect to the wrapped polygon.
def call(height, line_height)
width(@max_y - height - line_height, @max_y - height)
end
private
# 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.
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
# Prepare the segments and other data for later use.
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).
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
end
end
end