RenderSettings.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tika.renderer;
import java.io.Serializable;
import java.util.Objects;
/**
* How a document is rendered to pixels: the {@code "render"} block of {@code "pages"}. A
* renderer reads the effective settings from the {@link org.apache.tika.parser.ParseContext}
* (class-keyed), scoped there by the parser around each render call.
* <p>
* {@code dpi} is the target; {@code maxWidth}/{@code maxHeight} is a ceiling: render at
* {@code dpi}, scale down to fit if either bound is exceeded, aspect preserved, never enlarge.
* {@code maxImagePixels} skips a page that would still be larger than that; {@code minWidth}/
* {@code minHeight} skips one that would be smaller at the target dpi (a hairline rule, a spacer).
* <p>
* Every field is nullable so an instance can be an overlay: {@link #over} applies the set
* fields of one instance to another. {@link #defaults()} has every field set.
*
* @since Apache Tika 4.1
*/
public class RenderSettings implements Serializable {
private static final long serialVersionUID = 1L;
public static final int NO_LIMIT = -1;
private Integer dpi;
private ImageType imageType;
private ImageFormat imageFormat;
private Float imageQuality;
private Long maxImagePixels;
private Integer maxWidth;
private Integer maxHeight;
private Integer minWidth;
private Integer minHeight;
/** 300 dpi gray PNG at quality 0.5, at most 100 megapixels, no box, at least 2 x 2. */
public static RenderSettings defaults() {
RenderSettings settings = new RenderSettings();
settings.dpi = 300;
settings.imageType = ImageType.GRAY;
settings.imageFormat = ImageFormat.PNG;
settings.imageQuality = 0.5f;
settings.maxImagePixels = 100_000_000L;
settings.maxWidth = NO_LIMIT;
settings.maxHeight = NO_LIMIT;
settings.minWidth = 2;
settings.minHeight = 2;
return settings;
}
/** A copy of this with every set field of {@code overlay} applied; null overlay is this. */
public RenderSettings over(RenderSettings overlay) {
RenderSettings result = copy();
if (overlay == null) {
return result;
}
if (overlay.dpi != null) {
result.dpi = overlay.dpi;
}
if (overlay.imageType != null) {
result.imageType = overlay.imageType;
}
if (overlay.imageFormat != null) {
result.imageFormat = overlay.imageFormat;
}
if (overlay.imageQuality != null) {
result.imageQuality = overlay.imageQuality;
}
if (overlay.maxImagePixels != null) {
result.maxImagePixels = overlay.maxImagePixels;
}
if (overlay.maxWidth != null) {
result.maxWidth = overlay.maxWidth;
}
if (overlay.maxHeight != null) {
result.maxHeight = overlay.maxHeight;
}
if (overlay.minWidth != null) {
result.minWidth = overlay.minWidth;
}
if (overlay.minHeight != null) {
result.minHeight = overlay.minHeight;
}
return result;
}
/**
* This with {@code dpi}, {@code maxImagePixels}, {@code maxWidth} and {@code maxHeight}
* reduced to {@code ceiling}'s where they are looser; this instance if none is. A null
* field on either side is left alone. Used at trust boundaries so a request may tighten
* the operator's render limits but not loosen them.
*
* @since Apache Tika 4.2
*/
public RenderSettings tightenedTo(RenderSettings ceiling) {
Integer tDpi = dpi == null || ceiling.dpi == null ? dpi : Math.min(dpi, ceiling.dpi);
Long tPixels = tighter(maxImagePixels, ceiling.maxImagePixels);
Long tWidth = tighter(toLong(maxWidth), toLong(ceiling.maxWidth));
Long tHeight = tighter(toLong(maxHeight), toLong(ceiling.maxHeight));
if (Objects.equals(tDpi, dpi) && Objects.equals(tPixels, maxImagePixels)
&& Objects.equals(tWidth, toLong(maxWidth))
&& Objects.equals(tHeight, toLong(maxHeight))) {
return this;
}
RenderSettings result = copy();
result.dpi = tDpi;
result.maxImagePixels = tPixels;
result.maxWidth = tWidth == null ? null : tWidth.intValue();
result.maxHeight = tHeight == null ? null : tHeight.intValue();
return result;
}
/** The tighter of two limits where {@link #NO_LIMIT} means unlimited. */
private static Long tighter(Long requested, Long ceiling) {
if (requested == null || ceiling == null || ceiling == NO_LIMIT) {
return requested;
}
return requested == NO_LIMIT ? ceiling : Math.min(requested, ceiling);
}
private static Long toLong(Integer value) {
return value == null ? null : value.longValue();
}
public RenderSettings copy() {
RenderSettings result = new RenderSettings();
result.dpi = dpi;
result.imageType = imageType;
result.imageFormat = imageFormat;
result.imageQuality = imageQuality;
result.maxImagePixels = maxImagePixels;
result.maxWidth = maxWidth;
result.maxHeight = maxHeight;
result.minWidth = minWidth;
result.minHeight = minHeight;
return result;
}
@Override
public boolean equals(Object o) {
return o instanceof RenderSettings other && Objects.equals(dpi, other.dpi)
&& imageType == other.imageType && imageFormat == other.imageFormat
&& Objects.equals(imageQuality, other.imageQuality)
&& Objects.equals(maxImagePixels, other.maxImagePixels)
&& Objects.equals(maxWidth, other.maxWidth)
&& Objects.equals(maxHeight, other.maxHeight)
&& Objects.equals(minWidth, other.minWidth)
&& Objects.equals(minHeight, other.minHeight);
}
@Override
public int hashCode() {
return Objects.hash(dpi, imageType, imageFormat, imageQuality, maxImagePixels, maxWidth,
maxHeight, minWidth, minHeight);
}
/**
* The dpi a page of this size in points is rendered at: the target, reduced so the image
* fits the box, never raised. Fractional, so the box is met exactly. Requires every field set.
*/
public float effectiveDpi(double widthPoints, double heightPoints) {
double scale = 1.0;
if (maxWidth > 0 && widthPoints > 0) {
scale = Math.min(scale, maxWidth / (widthPoints / 72.0 * dpi));
}
if (maxHeight > 0 && heightPoints > 0) {
scale = Math.min(scale, maxHeight / (heightPoints / 72.0 * dpi));
}
return (float) Math.max(0.01, dpi * scale);
}
/** True when the page rendered at the target dpi would be smaller than the minimum. */
public boolean belowMinimum(double widthPoints, double heightPoints) {
return Math.ceil(widthPoints / 72.0 * dpi) < minWidth
|| Math.ceil(heightPoints / 72.0 * dpi) < minHeight;
}
/** Pixels of the page rendered at {@link #effectiveDpi}; what {@code maxImagePixels} bounds. */
public long estimatedPixels(double widthPoints, double heightPoints) {
return estimatedPixels(widthPoints, heightPoints, effectiveDpi(widthPoints, heightPoints));
}
/** Pixels of the page rendered at this dpi; saturates rather than overflowing. */
public static long estimatedPixels(double widthPoints, double heightPoints, double dpi) {
double pixels = Math.ceil(widthPoints / 72.0 * dpi) * Math.ceil(heightPoints / 72.0 * dpi);
return pixels >= Long.MAX_VALUE ? Long.MAX_VALUE : (long) pixels;
}
/** True when an image of this many pixels exceeds {@code maxImagePixels}. */
public boolean exceedsMaxPixels(long pixels) {
return maxImagePixels > 0 && pixels > maxImagePixels;
}
/**
* A pixel size scaled down to fit the box, aspect preserved, never enlarged: for renderers
* that start from a bitmap rather than a page size.
*/
public long[] fit(long width, long height) {
double scale = 1.0;
if (maxWidth > 0 && width > maxWidth) {
scale = Math.min(scale, (double) maxWidth / width);
}
if (maxHeight > 0 && height > maxHeight) {
scale = Math.min(scale, (double) maxHeight / height);
}
return new long[] { Math.max(1, Math.round(width * scale)),
Math.max(1, Math.round(height * scale)) };
}
public Integer getDpi() {
return dpi;
}
public void setDpi(Integer dpi) {
if (dpi != null && dpi < 1) {
throw new IllegalArgumentException("dpi must be at least 1, got: " + dpi);
}
this.dpi = dpi;
}
public ImageType getImageType() {
return imageType;
}
public void setImageType(ImageType imageType) {
this.imageType = imageType;
}
public ImageFormat getImageFormat() {
return imageFormat;
}
public void setImageFormat(ImageFormat imageFormat) {
this.imageFormat = imageFormat;
}
/**
* ImageIO's compression quality. For PNG an inverted effort knob, not fidelity: 1.0 writes
* an uncompressed file, 0.0 spends ~10x the time of 0.5 for a few percent smaller output.
*/
public Float getImageQuality() {
return imageQuality;
}
public void setImageQuality(Float imageQuality) {
if (imageQuality != null && (imageQuality < 0f || imageQuality > 1f)) {
throw new IllegalArgumentException(
"imageQuality must be between 0.0 and 1.0, got: " + imageQuality);
}
this.imageQuality = imageQuality;
}
public Long getMaxImagePixels() {
return maxImagePixels;
}
/** A page that would still be larger than this after the box is skipped; -1 for no limit. */
public void setMaxImagePixels(Long maxImagePixels) {
if (maxImagePixels != null && maxImagePixels < 1 && maxImagePixels != NO_LIMIT) {
throw new IllegalArgumentException(
"maxImagePixels must be -1 (no limit) or at least 1, got: " + maxImagePixels);
}
this.maxImagePixels = maxImagePixels;
}
public Integer getMaxWidth() {
return maxWidth;
}
/** Widest the image may be, in pixels; scaled down to fit, never enlarged; -1 for no bound. */
public void setMaxWidth(Integer maxWidth) {
this.maxWidth = bound("maxWidth", maxWidth);
}
public Integer getMaxHeight() {
return maxHeight;
}
public void setMaxHeight(Integer maxHeight) {
this.maxHeight = bound("maxHeight", maxHeight);
}
public Integer getMinWidth() {
return minWidth;
}
/** Narrower than this at the target dpi and the document is not rendered at all; 0 for none. */
public void setMinWidth(Integer minWidth) {
this.minWidth = minimum("minWidth", minWidth);
}
public Integer getMinHeight() {
return minHeight;
}
public void setMinHeight(Integer minHeight) {
this.minHeight = minimum("minHeight", minHeight);
}
private static Integer bound(String name, Integer value) {
if (value != null && value < 1 && value != NO_LIMIT) {
throw new IllegalArgumentException(
name + " must be -1 (no bound) or at least 1, got: " + value);
}
return value;
}
private static Integer minimum(String name, Integer value) {
if (value != null && value < 0) {
throw new IllegalArgumentException(name + " must be at least 0, got: " + value);
}
return value;
}
}