Support for character-level justification#6161
Conversation
|
How doe this work with ligatures? It looks like they were disabled in the example. |
|
@MDLC01 They appear to be treated as a single glyph, which makes sense.
(Testing with fi) I'm not sure what the expected behavior should be when both are enabled, but note that in this example the microjustification is what I would consider excessive. You could tastefully mix the two by using less microjustification. I think it makes sense to allow both to be enabled at the same time, which empowers the user to choose if this is acceptable behavior or not. |
|
Regarding the first limitation, I would argue that one of the major benefits of microtypography is avoiding hyphenation in the first place. By only considering tracking, kerning expansion and protrusion only after line breaking has been done, we lose out on this. As an aside, is this really a good name for this feature? In fact, this seems to be orthogonal to justification. Adjusting tracking can be used to lessen "raggedness" even without actual justification |
|
@Enivex I agree with your first point. It's not a perfect solution, but I had trouble with having the microtype change the layout, because the layout engine seems to prefer microtype over hyphenation in the vast majority of cases (including some I would have personally prevented). I'll keep at it and see if I can get something working. As to the naming, assuming all microtype justification features have been implemented, what are your thoughts on having a parameter in
The main difference between this and tracking is that these are dynamically set per line for the purpose of more uniform justification. |
|
That's a significant improvement! |
I should mention that
In my opinion they should eventually be on by default as long as the defaults are unintrusive. Perhaps not in an initial implementation and before we also have expansion and proper protrusion though. The combination of small amounts of each of these should almost universally be preferable to having them off. Edit: I don't know what a sensible choice is, but less than this. It should also likely be relative to the distance between characters (including any additional tracking). |
tingerrr
left a comment
There was a problem hiding this comment.
Nice and simple, great work!
|
I think I've made all the changes you've requested, mind giving this another review pass @tingerrr ? |
tingerrr
left a comment
There was a problem hiding this comment.
LGTM, let's see what Laurenz says.
|
Just here to say, I've used LaTeX for years primarily because I like how microtypset text looks. Excited to see it come here! |
|
I'm excited for this! |
|
For a more apples-to-apples comparison you would need to use fontspec with New Computer Modern |
|
I'm very positively surprised how simple this looks! Is it correct that this currently only involves tracking, but not stretching of glyphs? |
It's only tracking, yes |
|
Alright @tingerrr, This is everythings I planned to go into this PR. More enhancements to justification will come in later PRs by either me or some other enthusiast. The changes are as follows:
Example: #table(
stroke: none,
gutter: 1em,
columns: 3,
)[
*Default Justification (Pre-PR Behavior)*
// Equivalent to Old Justification Rules, to show they are the same.
][
*Justification With Auto Justification Limits*
][
*Justification With Custom Justification Limits*
][
#set par(justify: true)
#set par(justify: true, justification-limits: none) // equivalent statement
#lorem(100)
][
#set par(justify: true, justification-limits: auto)
#lorem(100)
][
#set par(justify: true, justification-limits: (glyph: (min: 99% + 0pt, max: 100% + 0pt), word: (min: 80% + 0pt, max: 133% + 0pt)))
#lorem(100)
]
P.S. |
Co-authored-by: Kirill Lukashev <kirill.lukashev.sic@gmail.com>
sicikh
left a comment
There was a problem hiding this comment.
Just minor formatting fix
| let mut word: Dict = dict.take("word")?.cast()?; | ||
| let word_min = word.take("min")?.cast()?; | ||
| let word_max = word.take("max")?.cast()?; | ||
| word.finish(&["min", "max"])?; |
There was a problem hiding this comment.
| word.finish(&["min", "max"])?; | |
| word.finish(&["min", "max"])?; |
oopsie, forgot to include leading spaces. How did it pass formatting checks though 🤔 same thing below
| let mut glyph: Dict = dict.take("glyph")?.cast()?; | ||
| let glyph_min = glyph.take("min")?.cast()?; | ||
| let glyph_max = glyph.take("max")?.cast()?; | ||
| glyph.finish(&["min", "max"])?; |
There was a problem hiding this comment.
| glyph.finish(&["min", "max"])?; | |
| glyph.finish(&["min", "max"])?; |
same thing as above with word.finish
laurmaedje
left a comment
There was a problem hiding this comment.
One comment on which I'd appreciate feedback. The PR also needs tests and documentation, but to make this possible in time for the release, we'd take care of that internally.
| stretchability: ( | ||
| Em::zero(), | ||
| (width / 2.0) * justification_limits.word_max.rel.get() | ||
| + Em::from_length(justification_limits.word_max.abs, font_size), | ||
| ), | ||
| shrinkability: ( | ||
| Em::zero(), | ||
| (width / 3.0) * justification_limits.word_min.rel.get() | ||
| + Em::from_length(justification_limits.word_min.abs, font_size), | ||
| ), |
There was a problem hiding this comment.
It seems odd to me that the calculation works differently for the space. Wouldn't it make more sense for the default value of justification-limits to be adjusted instead? Basically making this the default value:
#(
word: (
min: 100% * (2 / 3),
max: 150%,
),
glyph: (
min: 100%,
max: 100%,
)
)And then we'd remove all of the none and auto things and just add suggested values for glyph to the documentation.
I'd also (as suggested by someone on Discord) add support for folding such that #set par(justification-limits: (glyph: (min: 98%, max: 102%)) does not override the existing values for word and #set par(justification-limits: (glyph: (min: 95%)) not the existing value for glyph.max. Dealing with folding is always a bit tricky though, so I could handle that myself.
There was a problem hiding this comment.
My train of thought was that the justification-limits are relative to the internal layout algorithm. From a UX perspective, I feel that users should not need to know any magic numbers in order to modify the layout algorithm.
I imagine the most common mental model for this feature would be that 100% means 100% of the spacing determined by the internal layout engine. 200% min word limit would mean that the spaces should be able to compress to double what they're at before modifying this value.
Otherwise, as you suggested, we'd just have the default values written in the documentation.
However, the flip side of this coin is that if 200% meant 200% of the engine's default spacing rule, if someone wanted to explicitly say 200% of an actual non-justified space, they'd have to know about the internal 2/3 and then write the true 100% as 3/2 multiplied by their initially desired value.
This could also be written into the documentation, but I just think that the first case that I mentioned would happen more often than the second? But the second one is more confusing to the end user if that is their desired behavior.
It's up to you, I just wanted to outline my thinking.
|
I think it would be good to have checks for |
|
Just as a heads-up: We're working on this internally now, so please don't push further commits. There are some layout issues that still need figuring out, e.g. end-of-line glyphs being incorrectly adjusted. |
|
I have now finalized the PR. There are some crucial changes to the implementation and the API:
|
|
That sounds great!
Is there a reason why this is clamped instead of throwing an error? I'd rather have the compilation fail than my input being silently treated differently when I specify and invalid value. |
I can't check this fully at the site where you define the value as it is font size and glyph size dependant, so the error would be very non-local and it would fail at some random piece of text. |
|
I compiled the new version, I think it works good and the spacing is a little bit simpler to adjust than before (
To sum up: everything works good, but the error should get improved as shown above. |
Yeah, I noticed this, but didn't really feel like fixing it for this PR specifically because it is a problem across the whole of Typst with such kinds of dictionaries and should really be fixed holistically. However, it's true that it's especially bad in this case, so I implemented a workaround now. |
|
Thanks @diquah and also everyone else who helped move this forward! |





This implements a partial solution to #4693.
This PR adds a new property,
microjustification, topar. Microjustification allows kerning to be dynamically applied during justification to further justify an existing line. This added kerning will never exceedmicrojustification, but may be less depending on how justified the line already is. This is equivalent to "Letter Spacing" in Adobe InDesign, and it improves readability by creating more uniform blocks of text, especially for very narrow blocks.All glyphs have microjustification enabled by default, but this can be changed in the future.
Example
Notice that lines with excessively large spaces become more readable.
Limitations
Microjustification occurs only after a line has been initially drafted. This means microjustification will not change the layout of a paragraph. It only further justifies existing lines by adding spacing between glyphs.