
com.itextpdf.svg.utils.SvgCoordinateUtils Maven / Gradle / Ivy
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2024 Apryse Group NV
Authors: Apryse Software.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 this program. If not, see .
*/
package com.itextpdf.svg.utils;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.geom.Vector;
import com.itextpdf.layout.properties.UnitValue;
import com.itextpdf.styledxmlparser.css.util.CssDimensionParsingUtils;
import com.itextpdf.styledxmlparser.css.util.CssTypesValidationUtils;
import com.itextpdf.svg.SvgConstants;
import com.itextpdf.svg.SvgConstants.Values;
import com.itextpdf.svg.exceptions.SvgExceptionMessageConstant;
public class SvgCoordinateUtils {
/**
* Converts relative coordinates to absolute ones. Assumes that relative coordinates are represented by
* an array of coordinates with length proportional to the length of current coordinates array,
* so that current coordinates array is applied in segments to the relative coordinates array
*
* @param relativeCoordinates the initial set of coordinates
* @param currentCoordinates an array representing the point relative to which the relativeCoordinates are defined
* @return a String array of absolute coordinates, with the same length as the input array
*/
public static String[] makeRelativeOperatorCoordinatesAbsolute(String[] relativeCoordinates,
double[] currentCoordinates) {
if (relativeCoordinates.length % currentCoordinates.length != 0) {
throw new IllegalArgumentException(
SvgExceptionMessageConstant.COORDINATE_ARRAY_LENGTH_MUST_BY_DIVISIBLE_BY_CURRENT_COORDINATES_ARRAY_LENGTH);
}
String[] absoluteOperators = new String[relativeCoordinates.length];
for (int i = 0; i < relativeCoordinates.length; ) {
for (int j = 0; j < currentCoordinates.length; j++, i++) {
double relativeDouble = Double.parseDouble(relativeCoordinates[i]);
relativeDouble += currentCoordinates[j];
absoluteOperators[i] = SvgCssUtils.convertDoubleToString(relativeDouble);
}
}
return absoluteOperators;
}
/**
* Calculate the angle between two vectors
*
* @param vectorA first vector
* @param vectorB second vector
* @return angle between vectors in radians units
*/
public static double calculateAngleBetweenTwoVectors(Vector vectorA, Vector vectorB) {
return Math.acos((double) vectorA.dot(vectorB) / ((double) vectorA.length() * (double) vectorB.length()));
}
/**
* Returns absolute value for attribute in userSpaceOnUse coordinate system.
*
* @param attributeValue value of attribute.
* @param defaultValue default value.
* @param start start border for calculating percent value.
* @param length length for calculating percent value.
* @param em em value.
* @param rem rem value.
* @return absolute value in the userSpaceOnUse coordinate system.
*/
public static double getCoordinateForUserSpaceOnUse(String attributeValue, double defaultValue,
double start, double length, float em, float rem) {
double absoluteValue;
final UnitValue unitValue = CssDimensionParsingUtils.parseLengthValueToPt(attributeValue, em, rem);
if (unitValue == null) {
absoluteValue = defaultValue;
} else if (unitValue.getUnitType() == UnitValue.PERCENT) {
absoluteValue = start + (length * unitValue.getValue() / 100);
} else {
absoluteValue = unitValue.getValue();
}
return absoluteValue;
}
/**
* Returns a value relative to the object bounding box.
* We should only call this method for attributes with coordinates relative to the object bounding rectangle.
*
* @param attributeValue attribute value to parse
* @param defaultValue this value will be returned if an error occurs while parsing the attribute value
* @return if {@code attributeValue} is a percentage value, the given percentage of 1 will be returned.
* And if it's a valid value with a number, the number will be extracted from that value.
*/
public static double getCoordinateForObjectBoundingBox(String attributeValue, double defaultValue) {
if (CssTypesValidationUtils.isPercentageValue(attributeValue)) {
return CssDimensionParsingUtils.parseRelativeValue(attributeValue, 1);
}
if (CssTypesValidationUtils.isNumber(attributeValue)
|| CssTypesValidationUtils.isMetricValue(attributeValue)
|| CssTypesValidationUtils.isRelativeValue(attributeValue)) {
// if there is incorrect value metric, then we do not need to parse the value
int unitsPosition = CssDimensionParsingUtils.determinePositionBetweenValueAndUnit(attributeValue);
if (unitsPosition > 0) {
// We want to ignore the unit type how this is done in the "Google Chrome" approach
// which treats the "abstract coordinate system" in the coordinate metric measure,
// i.e. for value '0.5cm' the top/left of the object bounding box would be (1cm, 1cm),
// for value '0.5em' the top/left of the object bounding box would be (1em, 1em) and etc.
// no null pointer should be thrown as determine
return CssDimensionParsingUtils.parseDouble(attributeValue.substring(0, unitsPosition))
.doubleValue();
}
}
return defaultValue;
}
/**
* Returns the viewBox received after scaling and displacement given preserveAspectRatio.
*
* @param viewBox parsed viewBox rectangle. It should be a valid {@link Rectangle}
* @param currentViewPort current element view port. It should be a valid {@link Rectangle}
* @param align the alignment value that indicates whether to force uniform scaling
* and, if so, the alignment method to use in case the aspect ratio of
* the viewBox doesn't match the aspect ratio of the viewport. If align
* is {@code null} or align is invalid (i.e. not in the predefined list),
* then the default logic with align = "xMidYMid", and meetOrSlice = "meet" would be used
* @param meetOrSlice the way to scale the viewBox. If meetOrSlice is not {@code null} and invalid,
* then the default logic with align = "xMidYMid"
* and meetOrSlice = "meet" would be used, if meetOrSlice is {@code null}
* then default "meet" value would be used with the specified align
* @return the applied viewBox {@link Rectangle}
*/
public static Rectangle applyViewBox(Rectangle viewBox, Rectangle currentViewPort, String align,
String meetOrSlice) {
if (currentViewPort == null) {
throw new IllegalArgumentException(SvgExceptionMessageConstant.CURRENT_VIEWPORT_IS_NULL);
}
if (viewBox == null || viewBox.getWidth() <= 0 || viewBox.getHeight() <= 0) {
throw new IllegalArgumentException(SvgExceptionMessageConstant.VIEWBOX_IS_INCORRECT);
}
if (align == null || (
meetOrSlice != null && !Values.MEET.equals(meetOrSlice) && !Values.SLICE.equals(meetOrSlice)
)) {
return applyViewBox(viewBox, currentViewPort, Values.XMID_YMID, Values.MEET);
}
double scaleWidth;
double scaleHeight;
if (Values.NONE.equalsIgnoreCase(align)) {
scaleWidth = (double) currentViewPort.getWidth() / (double) viewBox.getWidth();
scaleHeight = (double) currentViewPort.getHeight() / (double) viewBox.getHeight();
} else {
double scale = getScaleWidthHeight(viewBox, currentViewPort, meetOrSlice);
scaleWidth = scale;
scaleHeight = scale;
}
// apply scale
Rectangle appliedViewBox = new Rectangle(viewBox.getX(), viewBox.getY(),
(float) ((double) viewBox.getWidth() * scaleWidth),
(float) ((double) viewBox.getHeight() * scaleHeight));
double minXOffset = (double) currentViewPort.getX() - (double) appliedViewBox.getX();
double minYOffset = (double) currentViewPort.getY() - (double) appliedViewBox.getY();
double midXOffset = (double) currentViewPort.getX() + ((double) currentViewPort.getWidth() / 2)
- ((double) appliedViewBox.getX() + ((double) appliedViewBox.getWidth() / 2));
double midYOffset = (double) currentViewPort.getY() + ((double) currentViewPort.getHeight() / 2)
- ((double) appliedViewBox.getY() + ((double) appliedViewBox.getHeight() / 2));
double maxXOffset = (double) currentViewPort.getX() + (double) currentViewPort.getWidth()
- ((double) appliedViewBox.getX() + (double) appliedViewBox.getWidth());
double maxYOffset = (double) currentViewPort.getY() + (double) currentViewPort.getHeight()
- ((double) appliedViewBox.getY() + (double) appliedViewBox.getHeight());
double xOffset;
double yOffset;
switch (align.toLowerCase()) {
case SvgConstants.Values.NONE:
case SvgConstants.Values.XMIN_YMIN:
xOffset = minXOffset;
yOffset = minYOffset;
break;
case SvgConstants.Values.XMIN_YMID:
xOffset = minXOffset;
yOffset = midYOffset;
break;
case SvgConstants.Values.XMIN_YMAX:
xOffset = minXOffset;
yOffset = maxYOffset;
break;
case SvgConstants.Values.XMID_YMIN:
xOffset = midXOffset;
yOffset = minYOffset;
break;
case SvgConstants.Values.XMID_YMAX:
xOffset = midXOffset;
yOffset = maxYOffset;
break;
case SvgConstants.Values.XMAX_YMIN:
xOffset = maxXOffset;
yOffset = minYOffset;
break;
case SvgConstants.Values.XMAX_YMID:
xOffset = maxXOffset;
yOffset = midYOffset;
break;
case SvgConstants.Values.XMAX_YMAX:
xOffset = maxXOffset;
yOffset = maxYOffset;
break;
case SvgConstants.Values.XMID_YMID:
xOffset = midXOffset;
yOffset = midYOffset;
break;
default:
return applyViewBox(viewBox, currentViewPort, Values.XMID_YMID, Values.MEET);
}
// apply offset
appliedViewBox.moveRight((float) xOffset);
appliedViewBox.moveUp((float) yOffset);
return appliedViewBox;
}
private static double getScaleWidthHeight(Rectangle viewBox, Rectangle currentViewPort,
String meetOrSlice) {
double scaleWidth = (double) currentViewPort.getWidth() / (double) viewBox.getWidth();
double scaleHeight = (double) currentViewPort.getHeight() / (double) viewBox.getHeight();
if (Values.SLICE.equalsIgnoreCase(meetOrSlice)) {
return Math.max(scaleWidth, scaleHeight);
} else if (Values.MEET.equalsIgnoreCase(meetOrSlice) || meetOrSlice == null) {
return Math.min(scaleWidth, scaleHeight);
} else {
// This code should be unreachable. We check for incorrect cases
// in the applyViewBox method and instead use the default implementation (xMidYMid meet).
throw new IllegalStateException(
SvgExceptionMessageConstant.MEET_OR_SLICE_ARGUMENT_IS_INCORRECT);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy