Skip to content

Commit

Permalink
utilities: fix issues related to conversion of lengths to pt (#84)
Browse files Browse the repository at this point in the history
* utilities: fix conversion of negative `length` lengths

The `convert-length-to-pt` function did not properly handle converting
negative `length`s to pt. They would get converted to 0pt because the
line drawn for measurement would (I presume) have a width of 0pt
when given a negative `length`.

* utilities: factor out pt conversions into functions

* utilities: simplify and fix conversion of relative lengths to pt

`convert-length-to-pt` had a couple of issues regarding relative
lengths.

When the `length` component has an em part, there would be an error
because Typst does not allow comparing `length`s with an em part with
those that do not.

It also did not account for ratios with a fraction (e.g., 1.5%).

This commit fixes those issues and, at the same time, simplifies the
implementation to re-use the other conversion functions for better
consistency.

* utilities: convert `convert-length-to-pt` variables to kebab case

* utilities: convert `length`s to pt without losing precision

Unfortunately, `repr` rounds the `pt` component of `length`s to 2
decimal places, which results in a loss of precision.

This commit rewrites the conversion of `length`s to `pt` without
relying on the numerical values in the `repr`. We instead draw and
measure lines.

* utilities: convert `relative` lengths to pt without losing precision

Typst rounds all length components, except `em` components, to 2 decimal
places for their `repr`, which results in a loss of precision.

This commit resolves the loss of precision. Some aspects of Typst's
behaviour are important:
- The `repr` of `em` is lossless.
- The "draw and measure a line" technique applied on `relative` lengths
  returns the length of the `length` component only (the ratio component
  is ignored due to limitations of the technique).

* utilities: assert `page-size` and `frac-total` are purely pt lengths

* tablex-test: add tests for `convert-length-to-pt`

* tablex-test: add tests for line expansion

* tablex-test: check return type of conversion to pt

* tablex-test: test precision of conversion to pt

* add issue number to tablex-test.typ

* pdf will be in separate commit
  • Loading branch information
dixslyf authored and PgBiel committed Dec 17, 2023
1 parent 7f7a905 commit ad84c79
Show file tree
Hide file tree
Showing 5 changed files with 386 additions and 72 deletions.
4 changes: 2 additions & 2 deletions src/col-row-size.typ
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@
#let determine-column-sizes(grid: (), page-width: 0pt, styles: none, columns: none, inset: none, align: auto, col-gutter: none) = {
let columns = columns.map(c => {
if type(c) in (_length_type, _rel_len_type, _ratio_type) {
convert-length-to-pt(c, styles: styles, page_size: page-width)
convert-length-to-pt(c, styles: styles, page-size: page-width)
} else if c == none {
0pt
} else {
Expand Down Expand Up @@ -478,7 +478,7 @@
#let determine-row-sizes(grid: (), page-height: 0pt, styles: none, columns: none, rows: none, align: auto, inset: none, row-gutter: none) = {
let rows = rows.map(r => {
if type(r) in (_length_type, _rel_len_type, _ratio_type) {
convert-length-to-pt(r, styles: styles, page_size: page-height)
convert-length-to-pt(r, styles: styles, page-size: page-height)
} else {
r
}
Expand Down
6 changes: 3 additions & 3 deletions src/option-parsing.typ
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
panic("'expand' argument to lines must be a pair (length, length).")
}

convert-length-to-pt(e, styles: styles, page_size: page-size)
convert-length-to-pt(e, styles: styles, page-size: page-size)
}
})
}
Expand Down Expand Up @@ -79,11 +79,11 @@
row-gutter = default-if-auto(row-gutter, 0pt)

if type(col-gutter) in (_length_type, _rel_len_type, _ratio_type) {
col-gutter = convert-length-to-pt(col-gutter, styles: styles, page_size: page-width)
col-gutter = convert-length-to-pt(col-gutter, styles: styles, page-size: page-width)
}

if type(row-gutter) in (_length_type, _rel_len_type, _ratio_type) {
row-gutter = convert-length-to-pt(row-gutter, styles: styles, page_size: page-width)
row-gutter = convert-length-to-pt(row-gutter, styles: styles, page-size: page-width)
}

(col: col-gutter, row: row-gutter)
Expand Down
5 changes: 5 additions & 0 deletions src/type-validators.typ
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@
type(len) in (_ratio_type, _fraction_type, _rel_len_type, _length_type) and "inf" in repr(len)
}

// Check if the given length has type '_length_type' and no 'em' component.
#let is-purely-pt-len(len) = {
type(len) == _length_type and "em" not in repr(len)
}

#let validate-cols-rows(columns, rows, items: ()) = {
if type(columns) == _int_type {
assert(columns >= 0, message: "Error: Cannot have a negative amount of columns.")
Expand Down
218 changes: 153 additions & 65 deletions src/utilities.typ
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
#import "type-validators.typ": *
// -- end imports --

// Typst 0.9.0 uses a minus sign ("−"; U+2212 MINUS SIGN) for negative numbers.
// Before that, it used a hyphen minus ("-"; U+002D HYPHEN MINUS), so we use
// regex alternation to match either of those.
#let NUMBER-REGEX-STRING = "(−|-)?\\d*\\.?\\d+"

// Which positions does a cell occupy
// (Usually just its own, but increases if colspan / rowspan
// is greater than 1)
Expand Down Expand Up @@ -114,88 +119,171 @@
previous
}

// Convert a certain (non-relative) length to pt
// Measure a length in pt by drawing a line and using the measure() function.
// This function will work for negative lengths as well.
//
// Note that for ratios, the measurement will be 0pt due to limitations of
// the "draw and measure" technique (wrapping the line in a box still returns 0pt;
// not sure if there is any viable way to measure a ratio). This also affects
// relative lengths — this function will only be able to measure the length component.
//
// styles: from style()
// page_size: equivalent to 100%
// frac_amount: amount of 'fr' specified
// frac_total: total space shared by fractions
#let convert-length-to-pt(
len,
styles: none, page_size: none, frac_amount: none, frac_total: none
) = {
page_size = 0pt + page_size
#let measure-pt(len, styles) = {
let measured-pt = measure(line(length: len), styles).width

if is-infinite-len(len) {
0pt // avoid the destruction of the universe
} else if type(len) == _length_type {
if "em" in repr(len) {
if styles == none {
panic("Cannot convert length to pt ('styles' not specified).")
}
// If the measured length is positive, `len` must have overall been positive.
// There's nothing else to be done, so return the measured length.
if measured-pt > 0pt {
return measured-pt
}

measure(line(length: len), styles).width + 0pt
} else {
len + 0pt // mm, in, pt
}
} else if type(len) == _ratio_type {
if page_size == none {
panic("Cannot convert ratio to pt ('page_size' not specified).")
}
// If we've reached this point, the previously measured length must have been `0pt`
// (drawing a line with a negative length will draw nothing, so measuring it will return `0pt`).
// Hence, `len` must either be `0pt` or negative.
// We multiply `len` by -1 to get a positive length, draw a line and measure it, then negate
// the measured length. This nicely handles the `0pt` case as well.
measured-pt = -measure(line(length: -len), styles).width
return measured-pt
}

if is-infinite-len(page_size) {
return 0pt // page has 'auto' size => % should return 0
}
// Convert a length of type length to pt.
//
// styles: from style()
#let convert-length-type-to-pt(len, styles: none) = {
// repr examples: "1pt", "1em", "0.5pt", "0.5em", "1pt + 1em", "-0.5pt + -0.5em"
if "em" not in repr(len) {
// No need to do any conversion because it must already be in pt.
return len
}

((len / 1%) / 100) * page_size + 0pt // e.g. 100% / 1% = 100; / 100 = 1; 1 * page_size
} else if type(len) == _fraction_type {
if frac_amount == none {
panic("Cannot convert fraction to pt ('frac_amount' not specified).")
}
// At this point, we will need to draw a line for measurement,
// so we need the styles.
if styles == none {
panic("Cannot convert length to pt ('styles' not specified).")
}

if frac_total == none {
panic("Cannot convert fraction to pt ('frac_total' not specified).")
}
return measure-pt(len, styles)
}

if frac_amount <= 0 or is-infinite-len(frac_total) {
return 0pt
}
// Convert a ratio type length to pt
//
// page-size: equivalent to 100%
#let convert-ratio-type-to-pt(len, page-size) = {
assert(
is-purely-pt-len(page-size),
message: "'page-size' should be a purely pt length"
)

if page-size == none {
panic("Cannot convert ratio to pt ('page-size' not specified).")
}

let len_per_frac = frac_total / frac_amount
if is-infinite-len(page-size) {
return 0pt // page has 'auto' size => % should return 0
}

(len_per_frac * (len / 1fr)) + 0pt
} else if type(len) == _rel_len_type {
if styles == none {
panic("Cannot convert relative length to pt ('styles' not specified).")
}
((len / 1%) / 100) * page-size + 0pt // e.g. 100% / 1% = 100; / 100 = 1; 1 * page-size
}

let ratio_regex = regex("^\\d+%")
let ratio = repr(len).find(ratio_regex)
// Convert a fraction type length to pt
//
// frac-amount: amount of 'fr' specified
// frac-total: total space shared by fractions
#let convert-fraction-type-to-pt(len, frac-amount, frac-total) = {
assert(
is-purely-pt-len(frac-total),
message: "'frac-total' should be a purely pt length"
)

if frac-amount == none {
panic("Cannot convert fraction to pt ('frac-amount' not specified).")
}

if ratio == none { // 2em + 5pt (doesn't contain 100% or something)
measure(line(length: len), styles).width
} else { // 100% + 2em + 5pt --> extract the "100%" part
if page_size == none {
panic("Cannot convert relative length to pt ('page_size' not specified).")
}
if frac-total == none {
panic("Cannot convert fraction to pt ('frac-total' not specified).")
}

// SAFETY: guaranteed to be a ratio by regex
let ratio_part = eval(ratio)
assert(type(ratio_part) == _ratio_type, message: "Eval didn't return a ratio")
if frac-amount <= 0 or is-infinite-len(frac-total) {
return 0pt
}

let other_part = len - ratio_part // get the (2em + 5pt) part
let len-per-frac = frac-total / frac-amount

let ratio_part_pt = if is-infinite-len(page_size) { 0pt } else { ((ratio_part / 1%) / 100) * page_size }
let other_part_pt = 0pt
(len-per-frac * (len / 1fr)) + 0pt
}

if other_part < 0pt {
other_part_pt = -measure(line(length: -other_part), styles).width
} else {
other_part_pt = measure(line(length: other_part), styles).width
}
// Convert a relative type length to pt
//
// styles: from style()
// page-size: equivalent to 100% (optional because the length may not have a ratio component)
#let convert-relative-type-to-pt(len, styles, page-size: none) = {
// We will need to draw a line for measurement later,
// so we need the styles.
if styles == none {
panic("Cannot convert relative length to pt ('styles' not specified).")
}

ratio_part_pt + other_part_pt + 0pt
}
// Note on precision: the `repr` for em components is precise, unlike
// other length components, which are rounded to a precision of 2.
// This is true up to Typst 0.9.0 and possibly later versions.
let em-regex = regex(NUMBER-REGEX-STRING + "em")
let em-part-repr = repr(len).find(em-regex)

// Calculate the length minus its em component.
// E.g., 1% + 1pt + 1em -> 1% + 1pt
let (em-part, len-minus-em) = if em-part-repr == none {
(0em, len)
} else {
// SAFETY: guaranteed to be a purely em length by regex
let em-part = eval(em-part-repr)
(em-part, len - em-part)
}

// This will give only the pt part of the length.
// E.g., 1% + 1pt -> 1pt
// See the documentation on measure-pt for more information.
let pt-part = measure-pt(len-minus-em, styles)

// Since we have the values of the em and pt components,
// we can calculate the ratio part.
let ratio-part = len-minus-em - pt-part
let ratio-part-pt = if ratio-part == 0% {
// No point doing `convert-ratio-type-to-pt` if there's no ratio component.
0pt
} else {
convert-ratio-type-to-pt(ratio-part, page-size)
}

// The length part is the pt part + em part.
// Note: we cannot use `len - ratio-part` as that returns a `_rel_len_type` value,
// not a `_length_type` value.
let length-part-pt = convert-length-type-to-pt(pt-part + em-part, styles: styles)

ratio-part-pt + length-part-pt
}

// Convert a certain (non-relative) length to pt
//
// styles: from style()
// page-size: equivalent to 100%
// frac-amount: amount of 'fr' specified
// frac-total: total space shared by fractions
#let convert-length-to-pt(
len,
styles: none, page-size: none, frac-amount: none, frac-total: none
) = {
page-size = 0pt + page-size

if is-infinite-len(len) {
0pt // avoid the destruction of the universe
} else if type(len) == _length_type {
convert-length-type-to-pt(len, styles: styles)
} else if type(len) == _ratio_type {
convert-ratio-type-to-pt(len, page-size)
} else if type(len) == _fraction_type {
convert-fraction-type-to-pt(len, frac-amount, frac-total)
} else if type(len) == _rel_len_type {
convert-relative-type-to-pt(len, styles, page-size: page-size)
} else {
panic("Cannot convert '" + type(len) + "' to length.")
}
Expand Down
Loading

0 comments on commit ad84c79

Please sign in to comment.