Skip to content

Commit

Permalink
src/ tests/: added Story.fit*(), for finding optimal rect for a story.
Browse files Browse the repository at this point in the history
  • Loading branch information
julian-smith-artifex-com committed Dec 10, 2023
1 parent 3837e79 commit 30b34ee
Show file tree
Hide file tree
Showing 3 changed files with 418 additions and 0 deletions.
106 changes: 106 additions & 0 deletions docs/story-class.rst
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,112 @@ Story
arg and instead return a PDF `Document` in which links have been
created for each internal html link.

.. method:: fit(rectfn, positionfn=None, pagefn=None)

Finds optimal rect that contains the story `self`.

Returns `(parameter, rect, n)`:
`parameter`:
the smallest value in range `pmin..pmax` where the rect
returned by `fn(parameter)` is large enough to contain the
story `self`. `None` if no solution because `pmax` is too
small.
`rect`:
The rect from `fn(parameter)`, or `None` if `parameter` is
`None`.
`n`:
Number of calls made to `self.place()`.

On successful return, the last call to `self.place()` will have been
with the returned rectangle, so `self.draw()` can be used directly.

:arg fn:
A callable taking a floating point `parameter` and returning a
`fitz.Rect()`. If the rect is empty, we assume the story will not
fit and do not call `self.place()`.

Must guarantee that `self.place()` behaves monotonically when
given rect `fn(parameter`) as `parameter` increases. This usually
means that both width and height increase or stay unchanged as
`parameter` increases.
:arg pmin:
Minimum parameter to consider; `None` for -infinity.
:arg pmax:
Maximum parameter to consider; `None` for +infinity.
:arg delta:
Maximum error in returned `parameter`.
:arg verbose:
If true we output diagnostics.

.. method:: fit_scale(self, rect, smin=0, smax=None, delta=0.001, verbose=False)

Finds smallest value `scale` in range `smin..smax` where `scale * rect`
is large enough to contain the story `self`.

Returns `scale, scale*rect, n`, where `n` is the number of calls made
to `self.place()`. Returns `None, None, n` if no solution because
`smax` is too small.

:arg width:
width of rect.
:arg height:
height of rect.
:arg smin:
Minimum scale to consider; must be >= 0.
:arg smax:
Maximum scale to consider, must be >= smin or `None` for
infinite.
:arg delta:
Maximum error in returned scale.
:arg verbose:
If true we output diagnostics.

.. method:: fit_height(self, width, hmin=0, hmax=None, origin=(0, 0), delta=0.001, verbose=False)

Finds smallest height in range `hmin..hmax` where a rect with size
`(width, height)` is large enough to contain the story `self`.

Returns `height, rect, n`, where `n` is the number of calls made to
`self.place()`. Returns `None, None, n` if no solution because `hmax`
is too small.

:arg width:
width of rect.
:arg hmin:
Minimum height to consider; must be >= 0.
:arg hmax:
Maximum height to consider, must be >= hmin or `None` for
infinite.
:arg origin:
`(x0, y0)` of rect.
:arg delta:
Maximum error in returned height.
:arg verbose:
If true we output diagnostics.

.. method:: fit_width(self, height, wmin=0, wmax=None, origin=(0, 0), delta=0.001, verbose=False)

Finds smallest width in range `wmin..wmax` where a rect with size
`(width, height)` is large enough to contain the story `self`.

Returns `width, rect, n`, where `n` is the number of calls made to
`self.place()`. Returns `None, None, n` if no solution because `wmax`
is too small or `height` is too small.

:arg height:
height of rect.
:arg wmin:
Minimum width to consider; must be >= 0.
:arg wmax:
Maximum width to consider, must be >= wmin or `None` for
infinite.
:arg origin:
`(x0, y0)` of rect.
:arg delta:
Maximum error in returned width.
:arg verbose:
If true we output diagnostics.


Element Positioning CallBack function
--------------------------------------
Expand Down
238 changes: 238 additions & 0 deletions src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12053,6 +12053,244 @@ def positionfn2(position):
stream.seek(0)
return Story.add_pdf_links(stream, positions)

def fit(self, fn, pmin=None, pmax=None, delta=0.001, verbose=False):
'''
Finds optimal rect that contains the story `self`.

Returns `(parameter, rect, n)`:
parameter:
the smallest value in range `pmin..pmax` where the rect
returned by `fn(parameter)` is large enough to contain the
story `self`. `None` if no solution because `pmax` is too
small.
rect:
The rect from `fn(parameter)`, or `None` if `parameter` is
`None`.
n:
Number of calls made to `self.place()`.

On successful return, the last call to `self.place()` will have been
with the returned rectangle, so `self.draw()` can be used directly.

Args:
fn:
A callable taking a floating point `parameter` and returning a
`fitz.Rect()`. If the rect is empty, we assume the story will
not fit and do not call `self.place()`.

Must guarantee that `self.place()` behaves monotonically when
given rect `fn(parameter`) as `parameter` increases. This
usually means that both width and height increase or stay
unchanged as `parameter` increases.
pmin:
Minimum parameter to consider; `None` for -infinity.
pmax:
Maximum parameter to consider; `None` for +infinity.
delta:
Maximum error in returned `parameter`.
verbose:
If true we output diagnostics.
'''
def log(text):
assert verbose
print(f'fit(): {text}')
sys.stdout.flush()

assert isinstance(pmin, (int, float)) or pmin is None
assert isinstance(pmax, (int, float)) or pmax is None

class State:
def __init__(self):
self.pmin = pmin
self.pmax = pmax
self.pmax_rect = None, None
self.p_last = None
self.numcalls = 0
if verbose:
self.pmin0 = pmin
self.pmax0 = pmax
state = State()

if verbose:
log(f'starting. {state.pmin=} {state.pmax=}.')

def ret():
if state.pmax is not None and state.p_last != state.pmax:
if verbose:
log(f'Calling update() with pmax, because was overwritten by later calls.')
big_enough = update(state.pmax)
assert big_enough
if verbose:
log(f'finished. {state.pmin0=} {state.pmax0=}: returning n={state.numcalls} {state.pmax=}')
return state.pmax, state.pmax_rect, state.numcalls

def update(parameter):
'''
Evaluates `more, _ = self.place(fn(parameter))`. If `more` is
false, then `rect` is big enought to contain `self` and we
set `state.pmax=parameter` and return True. Otherwise we set
`state.pmin=parameter` and return False.
'''
rect = fn(parameter)
assert isinstance(rect, Rect), f'{type(rect)=} {rect=}'
if rect.is_empty:
big_enough = False
if verbose:
log(f'update(): not calling self.place() because rect is empty.')
else:
self.reset()
more, _ = self.place(rect)
big_enough = not more
state.numcalls += 1
if verbose:
log(f'update(): called self.place(): {state.numcalls:>2d}: {more=} {parameter=} {rect=}.')
if big_enough:
state.pmax = parameter
state.pmax_rect = rect
else:
state.pmin = parameter
state.p_last = parameter
return big_enough

def opposite(p, direction):
'''
Returns same sign as `direction`, larger or smaller than `p` if
direction is positive or negative respectively.
'''
if p is None or p==0:
return direction
if direction * p > 0:
return 2 * p
return -p

if state.pmin is None:
# Find an initial finite pmin value.
if verbose: log(f'finding pmin.')
parameter = opposite(state.pmax, -1)
while 1:
if not update(parameter):
break
parameter *= 2
else:
if update(state.pmin):
log(f'{state.pmin=} is big enough.')
return ret()

if state.pmax is None:
# Find an initial finite pmax value.
if verbose: log(f'finding pmax.')
parameter = opposite(state.pmin, +1)
while 1:
if update(parameter):
break
parameter *= 2
else:
if not update(state.pmax):
# No solution possible.
state.pmax = None
if verbose: log(f'No solution possible {state.pmax=}.')
return ret()

# Do binary search in pmin..pmax.
if verbose: log(f'doing binary search with {state.pmin=} {state.pmax=}.')
while 1:
if state.pmax - state.pmin < delta:
return ret()
parameter = (state.pmin + state.pmax) / 2
update(parameter)


def fit_scale(self, rect, smin=0, smax=None, delta=0.001, verbose=False):
'''
Finds smallest value `scale` in range `smin..smax` where `scale * rect`
is large enough to contain the story `self`.

Returns `scale, scale*rect, n`, where `n` is the number of calls made
to `self.place()`. Returns `None, None, n` if no solution because
`smax` is too small.

Args:
width:
width of rect.
height:
height of rect.
smin:
Minimum scale to consider; must be >= 0.
smax:
Maximum scale to consider, must be >= smin or `None` for
infinite.
delta:
Maximum error in returned scale.
verbose:
If true we output diagnostics.
'''
x0, y0, x1, y1 = rect
width = x1 - x0
height = y1 - y0
def fn(scale):
return Rect(x0, y0, x0 + scale*width, y0 + scale*height)
return self.fit(fn, smin, smax, delta, verbose)

def fit_height(self, width, hmin=0, hmax=None, origin=(0, 0), delta=0.001, verbose=False):
'''
Finds smallest height in range `hmin..hmax` where a rect with size
`(width, height)` is large enough to contain the story `self`.

Returns `height, rect, n`, where `n` is the number of calls made to
`self.place()`. Returns `None, None, n` if no solution because `hmax`
is too small.

Args:
width:
width of rect.
hmin:
Minimum height to consider; must be >= 0.
hmax:
Maximum height to consider, must be >= hmin or `None` for
infinite.
origin:
`(x0, y0)` of rect.
delta:
Maximum error in returned height.
verbose:
If true we output diagnostics.
'''
x0, y0 = origin
x1 = x0 + width
def fn(height):
return Rect(x0, y0, x1, y0+height)
return self.fit(fn, hmin, hmax, delta, verbose)

def fit_width(self, height, wmin=0, wmax=None, origin=(0, 0), delta=0.001, verbose=False):
'''
Finds smallest width in range `wmin..wmax` where a rect with size
`(width, height)` is large enough to contain the story `self`.

Returns `width, rect, n`, where `n` is the number of calls made to
`self.place()`. Returns `None, None, n` if no solution because `wmax`
is too small or `height` is too small.

Args:
height:
height of rect.
wmin:
Minimum width to consider; must be >= 0.
wmax:
Maximum width to consider, must be >= wmin or `None` for
infinite.
origin:
`(x0, y0)` of rect.
delta:
Maximum error in returned width.
verbose:
If true we output diagnostics.
'''
x0, y0 = origin
y1 = x0 + height
def fn(width):
return Rect(x0, y0, x0+width, y1)
return self.fit(fn, wmin, wmax, delta, verbose)


class TextPage:

Expand Down
Loading

0 comments on commit 30b34ee

Please sign in to comment.