Skip to content

Commit 09b077c

Browse files
ar3emyulian-gaponenko
authored andcommitted
Implement HTML-like line-stacking algorithm in layout.
Implement ascender/descender calculation according to CSS specification. DEVSIX-3660 DEVSIX-3661
1 parent 6a80e7e commit 09b077c

21 files changed

+930
-41
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2020 iText Group NV
4+
Authors: iText Software.
5+
6+
This program is offered under a commercial and under the AGPL license.
7+
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
8+
9+
AGPL licensing:
10+
This program is free software: you can redistribute it and/or modify
11+
it under the terms of the GNU Affero General Public License as published by
12+
the Free Software Foundation, either version 3 of the License, or
13+
(at your option) any later version.
14+
15+
This program is distributed in the hope that it will be useful,
16+
but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
GNU Affero General Public License for more details.
19+
20+
You should have received a copy of the GNU Affero General Public License
21+
along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
package com.itextpdf.layout.property;
24+
25+
/**
26+
* A property corresponding to the css line-height property and used to
27+
* set the height of a line box in the HTML mode. On block-level elements,
28+
* it specifies the minimum height of line boxes within the element.
29+
* On non-replaced inline elements, it specifies the height that is used to calculate line box height.
30+
*/
31+
public class LineHeight {
32+
private static final int FIXED = 1;
33+
private static final int MULTIPLIED = 2;
34+
private static final int NORMAL = 4;
35+
36+
private int type;
37+
private float value;
38+
39+
private LineHeight(int type, float value) {
40+
this.type = type;
41+
this.value = value;
42+
}
43+
44+
/**
45+
* Returns the line height value.
46+
* The meaning of the returned value depends on the type of line height.
47+
*
48+
* @return the {@link LineHeight} value.
49+
*/
50+
public float getValue() {
51+
return value;
52+
}
53+
54+
/**
55+
* Creates a {@link LineHeight} with a fixed value.
56+
*
57+
* @param value value to set
58+
* @return created {@link LineHeight} object
59+
*/
60+
public static LineHeight createFixedValue(float value) {
61+
return new LineHeight(FIXED, value);
62+
}
63+
64+
/**
65+
* Creates a {@link LineHeight} with multiplied value.
66+
* This value must be multiplied by the element's font size.
67+
*
68+
* @param value value to set
69+
* @return created {@link LineHeight} object
70+
*/
71+
public static LineHeight createMultipliedValue(float value) {
72+
return new LineHeight(MULTIPLIED, value);
73+
}
74+
75+
/**
76+
* Creates a normal {@link LineHeight}.
77+
*
78+
* @return created {@link LineHeight} object
79+
*/
80+
public static LineHeight createNormalValue() {
81+
return new LineHeight(NORMAL, 0);
82+
}
83+
84+
/**
85+
* Check if the {@link LineHeight} contains fixed value.
86+
*
87+
* @return true if {@link LineHeight} contains fixed value.
88+
*/
89+
public boolean isFixedValue() {
90+
return type == FIXED;
91+
}
92+
93+
/**
94+
* Check if the {@link LineHeight} contains multiplied value.
95+
*
96+
* @return true if {@link LineHeight} contains multiplied value.
97+
*/
98+
public boolean isMultipliedValue() {
99+
return type == MULTIPLIED;
100+
}
101+
102+
/**
103+
* Check if the {@link LineHeight} contains normal value.
104+
*
105+
* @return true if {@link LineHeight} is normal.
106+
*/
107+
public boolean isNormalValue() {
108+
return type == NORMAL;
109+
}
110+
}

layout/src/main/java/com/itextpdf/layout/property/Property.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ private Property() {
136136
public static final int LEADING = 33;
137137
public static final int LEFT = 34;
138138
public static final int LINE_DRAWER = 35;
139+
public static final int LINE_HEIGHT = 124;
139140
public static final int LINK_ANNOTATION = 88;
140141
public static final int LIST_START = 36;
141142
public static final int LIST_SYMBOL = 37;
@@ -174,6 +175,7 @@ private Property() {
174175
public static final int PADDING_TOP = 50;
175176
public static final int PAGE_NUMBER = 51;
176177
public static final int POSITION = 52;
178+
public static final int RENDERING_MODE = 123;
177179
public static final int RIGHT = 54;
178180
public static final int ROTATION_ANGLE = 55;
179181
public static final int ROTATION_INITIAL_HEIGHT = 56;
@@ -219,7 +221,7 @@ private Property() {
219221
* related to textual operations. Indicates whether or not this type of property is inheritable.
220222
*/
221223
private static final boolean[] INHERITED_PROPERTIES;
222-
private static final int MAX_INHERITED_PROPERTY_ID = 122;
224+
private static final int MAX_INHERITED_PROPERTY_ID = 124;
223225

224226
static {
225227
INHERITED_PROPERTIES = new boolean[MAX_INHERITED_PROPERTY_ID + 1];
@@ -259,6 +261,8 @@ private Property() {
259261
INHERITED_PROPERTIES[Property.WORD_SPACING] = true;
260262
INHERITED_PROPERTIES[Property.TAGGING_HELPER] = true;
261263
INHERITED_PROPERTIES[Property.TYPOGRAPHY_CONFIG] = true;
264+
INHERITED_PROPERTIES[Property.RENDERING_MODE] = true;
265+
INHERITED_PROPERTIES[Property.LINE_HEIGHT] = true;
262266
}
263267

264268
/**
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2020 iText Group NV
4+
Authors: iText Software.
5+
6+
This program is offered under a commercial and under the AGPL license.
7+
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
8+
9+
AGPL licensing:
10+
This program is free software: you can redistribute it and/or modify
11+
it under the terms of the GNU Affero General Public License as published by
12+
the Free Software Foundation, either version 3 of the License, or
13+
(at your option) any later version.
14+
15+
This program is distributed in the hope that it will be useful,
16+
but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
GNU Affero General Public License for more details.
19+
20+
You should have received a copy of the GNU Affero General Public License
21+
along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
package com.itextpdf.layout.property;
24+
25+
26+
/**
27+
* Enum of rendering modes that can be used in layout logic.
28+
*/
29+
public enum RenderingMode {
30+
31+
/**
32+
* Default object layout mode
33+
*/
34+
DEFAULT_LAYOUT_MODE,
35+
36+
/**
37+
* Mode in which objects are processed in accordance with the HTML documentation
38+
*/
39+
HTML_MODE
40+
}

layout/src/main/java/com/itextpdf/layout/renderer/AbstractRenderer.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ This file is part of the iText (R) project.
7272
import com.itextpdf.layout.font.FontCharacteristics;
7373
import com.itextpdf.layout.font.FontFamilySplitter;
7474
import com.itextpdf.layout.font.FontProvider;
75+
import com.itextpdf.layout.font.FontSet;
7576
import com.itextpdf.layout.layout.LayoutArea;
7677
import com.itextpdf.layout.layout.LayoutContext;
7778
import com.itextpdf.layout.layout.LayoutPosition;
@@ -89,15 +90,15 @@ This file is part of the iText (R) project.
8990
import com.itextpdf.layout.property.Transform;
9091
import com.itextpdf.layout.property.TransparentColor;
9192
import com.itextpdf.layout.property.UnitValue;
92-
import org.slf4j.Logger;
93-
import org.slf4j.LoggerFactory;
9493

9594
import java.util.ArrayList;
9695
import java.util.Arrays;
9796
import java.util.Collections;
9897
import java.util.HashMap;
9998
import java.util.List;
10099
import java.util.Map;
100+
import org.slf4j.Logger;
101+
import org.slf4j.LoggerFactory;
101102

102103
/**
103104
* Defines the most common properties and behavior that are shared by most
@@ -2168,8 +2169,12 @@ PdfFont resolveFirstPdfFont() {
21682169
if (provider == null) {
21692170
throw new IllegalStateException(PdfException.FontProviderNotSetFontFamilyNotResolved);
21702171
}
2172+
FontSet fontSet = this.<FontSet>getProperty(Property.FONT_SET);
2173+
if (provider.getFontSet().isEmpty() && (fontSet == null || fontSet.isEmpty())) {
2174+
throw new IllegalStateException(PdfException.FontProviderNotSetFontFamilyNotResolved);
2175+
}
21712176
FontCharacteristics fc = createFontCharacteristics();
2172-
return resolveFirstPdfFont((String[]) font, provider, fc);
2177+
return provider.getPdfFont(provider.getFontSelector(Arrays.asList((String[]) font), fc, fontSet).bestMatch(), fontSet);
21732178
} else {
21742179
throw new IllegalStateException("String[] or PdfFont expected as value of FONT property");
21752180
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2020 iText Group NV
4+
Authors: iText Software.
5+
6+
This program is offered under a commercial and under the AGPL license.
7+
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
8+
9+
AGPL licensing:
10+
This program is free software: you can redistribute it and/or modify
11+
it under the terms of the GNU Affero General Public License as published by
12+
the Free Software Foundation, either version 3 of the License, or
13+
(at your option) any later version.
14+
15+
This program is distributed in the hope that it will be useful,
16+
but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
GNU Affero General Public License for more details.
19+
20+
You should have received a copy of the GNU Affero General Public License
21+
along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
package com.itextpdf.layout.renderer;
24+
25+
import com.itextpdf.io.font.FontMetrics;
26+
import com.itextpdf.io.font.FontProgram;
27+
import com.itextpdf.kernel.font.PdfFont;
28+
import com.itextpdf.layout.property.LineHeight;
29+
import com.itextpdf.layout.property.Property;
30+
31+
class LineHeightHelper {
32+
private static float DEFAULT_LINE_HEIGHT_COEFF = 1.15f;
33+
34+
private LineHeightHelper() {
35+
}
36+
37+
static float[] getActualAscenderDescender(AbstractRenderer renderer) {
38+
float ascender;
39+
float descender;
40+
float lineHeight = LineHeightHelper.calculateLineHeight(renderer);
41+
float[] fontAscenderDescender = LineHeightHelper.getFontAscenderDescenderNormalized(renderer);
42+
float leading = lineHeight - (fontAscenderDescender[0] - fontAscenderDescender[1]);
43+
ascender = fontAscenderDescender[0] + leading / 2f;
44+
descender = fontAscenderDescender[1] - leading / 2f;
45+
return new float[] {ascender, descender};
46+
}
47+
48+
static float[] getFontAscenderDescenderNormalized(AbstractRenderer renderer) {
49+
PdfFont font = renderer.resolveFirstPdfFont();
50+
float fontSize = renderer.getPropertyAsUnitValue(Property.FONT_SIZE).getValue();
51+
float[] fontAscenderDescenderFromMetrics = calculateFontAscenderDescenderFromFontMetrics(font);
52+
float fontAscender = fontAscenderDescenderFromMetrics[0] / FontProgram.UNITS_NORMALIZATION * fontSize;
53+
float fontDescender = fontAscenderDescenderFromMetrics[1] / FontProgram.UNITS_NORMALIZATION * fontSize;
54+
return new float[] {fontAscender, fontDescender};
55+
}
56+
57+
static float calculateLineHeight(AbstractRenderer renderer) {
58+
LineHeight lineHeight = renderer.<LineHeight>getProperty(Property.LINE_HEIGHT);
59+
float fontSize = renderer.getPropertyAsUnitValue(Property.FONT_SIZE).getValue();
60+
float lineHeightValue;
61+
if (lineHeight == null || lineHeight.isNormalValue() || lineHeight.getValue() < 0) {
62+
lineHeightValue = DEFAULT_LINE_HEIGHT_COEFF * fontSize;
63+
float[] fontAscenderDescender = getFontAscenderDescenderNormalized(renderer);
64+
float fontAscenderDescenderSum = fontAscenderDescender[0] - fontAscenderDescender[1];
65+
if (fontAscenderDescenderSum > lineHeightValue) {
66+
lineHeightValue = fontAscenderDescenderSum;
67+
}
68+
} else {
69+
if (lineHeight.isFixedValue()) {
70+
lineHeightValue = lineHeight.getValue();
71+
} else {
72+
lineHeightValue = lineHeight.getValue() * fontSize;
73+
}
74+
}
75+
return lineHeightValue;
76+
}
77+
78+
static float[] calculateFontAscenderDescenderFromFontMetrics(PdfFont font) {
79+
FontMetrics fontMetrics = font.getFontProgram().getFontMetrics();
80+
float ascender;
81+
float descender;
82+
if (fontMetrics.getWinAscender() == 0 || fontMetrics.getWinDescender() == 0 ||
83+
fontMetrics.getTypoAscender() == fontMetrics.getWinAscender()
84+
&& fontMetrics.getTypoDescender() == fontMetrics.getWinDescender()) {
85+
ascender = fontMetrics.getTypoAscender();
86+
descender = fontMetrics.getTypoDescender();
87+
} else {
88+
ascender = fontMetrics.getWinAscender();
89+
descender = fontMetrics.getWinDescender();
90+
}
91+
return new float[] {ascender, descender};
92+
}
93+
}

layout/src/main/java/com/itextpdf/layout/renderer/LineRenderer.java

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,9 @@ This file is part of the iText (R) project.
6565
import com.itextpdf.layout.property.Leading;
6666
import com.itextpdf.layout.property.OverflowPropertyValue;
6767
import com.itextpdf.layout.property.Property;
68+
import com.itextpdf.layout.property.RenderingMode;
6869
import com.itextpdf.layout.property.TabAlignment;
6970
import com.itextpdf.layout.property.UnitValue;
70-
import org.slf4j.Logger;
71-
import org.slf4j.LoggerFactory;
7271

7372
import java.util.ArrayList;
7473
import java.util.Arrays;
@@ -77,6 +76,8 @@ This file is part of the iText (R) project.
7776
import java.util.List;
7877
import java.util.Map;
7978
import java.util.NavigableMap;
79+
import org.slf4j.Logger;
80+
import org.slf4j.LoggerFactory;
8081

8182
public class LineRenderer extends AbstractRenderer {
8283

@@ -125,12 +126,20 @@ public LayoutResult layout(LayoutContext layoutContext) {
125126
occupiedArea = new LayoutArea(layoutContext.getArea().getPageNumber(), layoutBox.clone().moveUp(layoutBox.getHeight()).setHeight(0).setWidth(0));
126127

127128
float curWidth = 0;
128-
maxAscent = 0;
129-
maxDescent = 0;
129+
if (RenderingMode.HTML_MODE.equals(this.<RenderingMode>getProperty(Property.RENDERING_MODE))
130+
&& hasProperty(Property.LINE_HEIGHT)) {
131+
float[] ascenderDescender = LineHeightHelper.getActualAscenderDescender(this);
132+
maxAscent = ascenderDescender[0];
133+
maxDescent = ascenderDescender[1];
134+
} else {
135+
maxAscent = 0;
136+
maxDescent = 0;
137+
}
130138
maxTextAscent = 0;
131139
maxTextDescent = 0;
132140
maxBlockAscent = -1e20f;
133141
maxBlockDescent = 1e20f;
142+
134143
int childPos = 0;
135144

136145
MinMaxWidth minMaxWidth = new MinMaxWidth();
@@ -374,9 +383,18 @@ public LayoutResult layout(LayoutContext layoutContext) {
374383

375384
float childAscent = 0;
376385
float childDescent = 0;
377-
if (childRenderer instanceof ILeafElementRenderer && childResult.getStatus() != LayoutResult.NOTHING) {
378-
childAscent = ((ILeafElementRenderer) childRenderer).getAscent();
379-
childDescent = ((ILeafElementRenderer) childRenderer).getDescent();
386+
if (childRenderer instanceof ILeafElementRenderer
387+
&& childResult.getStatus() != LayoutResult.NOTHING) {
388+
if (RenderingMode.HTML_MODE.equals(childRenderer.<RenderingMode>getProperty(Property.RENDERING_MODE))
389+
&& childRenderer instanceof TextRenderer) {
390+
float[] ascenderDescender = LineHeightHelper
391+
.getActualAscenderDescender((AbstractRenderer) childRenderer);
392+
childAscent = ascenderDescender[0];
393+
childDescent = ascenderDescender[1];
394+
} else {
395+
childAscent = ((ILeafElementRenderer) childRenderer).getAscent();
396+
childDescent = ((ILeafElementRenderer) childRenderer).getDescent();
397+
}
380398
} else if (isInlineBlockChild && childResult.getStatus() != LayoutResult.NOTHING) {
381399
if (childRenderer instanceof AbstractRenderer) {
382400
Float yLine = ((AbstractRenderer) childRenderer).getLastYLineRecursively();

0 commit comments

Comments
 (0)