Thumbnails git
https://github.com/coobird/thumbnailator
Thumbnails API文档
https://coobird.github.io/thumbnailator/javadoc/0.4.13/
探究压缩系数scale与实际图片压缩比例之间的关系。
以下为目前最新版本0.4.19代码,2022-12-31日发布。
Thumbnails.of(BufferedImage)方法返回Builder。
其作用是创建其内部类Builder,图片流相关处理都在Builder中进行。
设置压缩系数,更新配置状态
Build.scale(double scale)方法调用scale(width, height)将scale值赋值给scaleWidth和scaleHeight,并更新配置状态。
由此可见,压缩时长宽都会乘以压缩系数进行相应比例压缩,所以原始图片像素scalescale=目标图片像素。
asBufferedImage()方法主要分两部分:
1.checkReadiness()检查配置。
2.Thumbnailator.createThumbnail()。核心功能压缩图片在第二部分。
这部分代码分为以下部分:
1)makeParam(),将参数组装ThumbnailParameter对象,主要是scaleWidth和scaleHeight。
2)创建SourceSinkThumbnailTask对象。
3)createThumbnail(),选择压缩器,压缩图片核心代码。
Thumbnailator.createThumbnail(
new SourceSinkThumbnailTask(makeParam(), source, destination)
);
将参数组装ThumbnailParameter对象,主要是scaleWidth和scaleHeight。组装缩放器工厂类ResizerFactory。
/**
* Returns a {@link ThumbnailParameter} from the current builder state.
*
* @return A {@link ThumbnailParameter} from the current
* builder state.
*/
private ThumbnailParameter makeParam() {
prepareResizerFactory();
int imageTypeToUse = imageType;
if (imageType == IMAGE_TYPE_UNSPECIFIED) {
imageTypeToUse = ThumbnailParameter.ORIGINAL_IMAGE_TYPE;
}
/*
* croppingPosition being non-null means that a crop should
* take place.
*/
if (croppingPosition != null) {
filterPipeline.addFirst(new Canvas(width, height, croppingPosition));
}
if (Double.isNaN(scaleWidth)) {
// If the dimensions were specified, do the following.
// Check that at least one dimension is specified.
// If it's not, it's a bug.
if (
width == DIMENSION_NOT_SPECIFIED &&
height == DIMENSION_NOT_SPECIFIED
) {
throw new IllegalStateException(
"The width or height must be specified. If this " +
"exception is thrown, it is due to a bug in the " +
"Thumbnailator library."
);
}
// Set the unspecified dimension to a default value.
if (width == DIMENSION_NOT_SPECIFIED) {
width = Integer.MAX_VALUE;
}
if (height == DIMENSION_NOT_SPECIFIED) {
height = Integer.MAX_VALUE;
}
return new ThumbnailParameter(
new Dimension(width, height),
sourceRegion,
keepAspectRatio,
outputFormat,
outputFormatType,
outputQuality,
imageTypeToUse,
filterPipeline.getFilters(),
resizerFactory,
fitWithinDimenions,
useExifOrientation
);
} else {
// If the scaling factor was specified
return new ThumbnailParameter(
scaleWidth,
scaleHeight,
sourceRegion,
keepAspectRatio,
outputFormat,
outputFormatType,
outputQuality,
imageTypeToUse,
filterPipeline.getFilters(),
resizerFactory,
fitWithinDimenions,
useExifOrientation
);
}
}
如果未设置ScalingMode,默认使用DefaultResizerFactory。
压缩模式,分为3种;BILINEAR、BICUBIC、PROGRESSIVE_BILINEAR。
前2个压缩算法resize()方法完全一致,属于双线性压缩。
默认压缩算法当压缩比例小于一半时,会采用PROGRESSIVE_BILINEAR。
public enum ScalingMode {
/**
* A hint to use bilinear interpolation when resizing images.
*/
BILINEAR,
/**
* A hint to use bicubic interpolation when resizing images.
*/
BICUBIC,
/**
* A hint to use progressing bilinear interpolation when resizing images.
*
* For details on this technique, refer to the documentation of the
* {@link ProgressiveBilinearResizer} class.
*/
PROGRESSIVE_BILINEAR,
;
}
默认压缩工厂,会根据目前图片文件大小与目标文件大小比例选择对应压缩器。
public class DefaultResizerFactory implements ResizerFactory {
private static final DefaultResizerFactory INSTANCE = new DefaultResizerFactory();
/**
* This class is not intended to be instantiated via the constructor.
*/
private DefaultResizerFactory() {}
/**
* Returns an instance of this class.
*
* @return An instance of this class.
*/
public static ResizerFactory getInstance() {
return INSTANCE;
}
public Resizer getResizer() {
return Resizers.PROGRESSIVE;
}
public Resizer getResizer(Dimension originalSize, Dimension thumbnailSize) {
int origWidth = originalSize.width;
int origHeight = originalSize.height;
int thumbWidth = thumbnailSize.width;
int thumbHeight = thumbnailSize.height;
if (thumbWidth < origWidth && thumbHeight < origHeight) {
if (thumbWidth < (origWidth / 2) && thumbHeight < (origHeight / 2)) {
// 当目标长宽压缩不到原长度一半时,使用PROGRESSIVE算法
return Resizers.PROGRESSIVE;
} else {
// 当目标长宽压缩后在小于原长度,并且大于原长度一半时,使用BILINEAR算法
return Resizers.BILINEAR;
}
}
else if (thumbWidth > origWidth && thumbHeight > origHeight) {
// 当图片放大时,使用BICUBIC算法
return Resizers.BICUBIC;
} else if (thumbWidth == origWidth && thumbHeight == origHeight) {
return Resizers.NULL;
} else {
return getResizer();
}
}
}
SourceSinkThumbnailTask主要用来传递参数配置,源文件流,目标文件流等信息。
new SourceSinkThumbnailTask
*
* A {@link ThumbnailTask} which holds an {@link ImageSource} from which the
* image is read or retrieved, and an {@link ImageSink} to which the thumbnail
* is stored or written.
* <p>
* This class will take care of handing off information from the
* {@link ImageSource} to the {@link ImageSink}. For example, the output format
* that should be used by the {@link ImageSink} will be handed off if the
* {@link ThumbnailParameter#ORIGINAL_FORMAT} parameter is set.
*
* @author coobird
*
* @param <S> The source class from which the source image is retrieved
* or read.
* @param <D> The destination class to which the thumbnail is stored
* or written.
*/
public class SourceSinkThumbnailTask<S, D> extends ThumbnailTask<S, D> {
/**
* The source from which the image is retrieved or read.
*/
private final ImageSource<S> source;
/**
* The destination to which the thumbnail is stored or written.
*/
private final ImageSink<D> destination;
/**
* Creates a {@link ThumbnailTask} in which an image is retrived from the
* specified {@link ImageSource} and written to the specified
* {@link ImageSink}, using the parameters provided in the specified
* {@link ThumbnailParameter}.
*
* @param param The parameters to use to create the thumbnail.
* @param source The source from which the image is retrieved
* or read from.
* @param destination The destination to which the thumbnail is
* stored or written to.
* @throws NullPointerException If either the parameter,
* {@link ImageSource} or {@link ImageSink}
* is {@code null}.
*/
public SourceSinkThumbnailTask(ThumbnailParameter param, ImageSource<S> source, ImageSink<D> destination) {
super(param);
if (source == null) {
throw new NullPointerException("ImageSource cannot be null.");
}
if (destination == null) {
throw new NullPointerException("ImageSink cannot be null.");
}
source.setThumbnailParameter(param);
this.source = source;
destination.setThumbnailParameter(param);
this.destination = destination;
}
/**
* Creates a thumbnail from parameters specified in a {@link ThumbnailTask}.
*
* @param task A {@link ThumbnailTask} to execute.
* @throws IOException Thrown when a problem occurs when creating a
* thumbnail.
*/
public static void createThumbnail(ThumbnailTask<?, ?> task) throws IOException {
ThumbnailParameter param = task.getParam();
// Obtain the original image.
BufferedImage sourceImage = task.read();
// Decide the image type of the destination image.
int imageType = param.getType();
/*
* If the imageType indicates that the image type of the original image
* should be used in the thumbnail, then obtain the image type of the
* original.
*
* If the original type is a custom type, then the default image type
* will be used.
*/
if (param.useOriginalImageType()) {
int imageTypeToUse = sourceImage.getType();
if (imageTypeToUse == BufferedImage.TYPE_CUSTOM) {
imageType = ThumbnailParameter.DEFAULT_IMAGE_TYPE;
} else {
imageType = sourceImage.getType();
}
}
// Check for presence of marker indicating to swap the width and height.
boolean isSwapDimensions = hasSwapDimensionsFilter(param.getImageFilters());
BufferedImage destinationImage;
// 目前通过改变scale进行图片缩放,size不改变,这里会执行else if逻辑分支
if (param.getSize() != null) {
// Get the dimensions of the original and thumbnail images.
Dimension size = param.getSize();
int destinationWidth = !isSwapDimensions ? size.width : size.height;
int destinationHeight = !isSwapDimensions ? size.height : size.width;
// Create the thumbnail.
destinationImage =
new FixedSizeThumbnailMaker()
.size(destinationWidth, destinationHeight)
.keepAspectRatio(param.isKeepAspectRatio())
.fitWithinDimensions(param.fitWithinDimenions())
.imageType(imageType)
.resizerFactory(param.getResizerFactory())
.make(sourceImage);
} else if (!Double.isNaN(param.getWidthScalingFactor())) {
// 设置了压缩系数会执行当前逻辑分支
// Create the thumbnail.
double widthScalingFactor = !isSwapDimensions ?
param.getWidthScalingFactor() : param.getHeightScalingFactor();
double heightScalingFactor = !isSwapDimensions ?
param.getHeightScalingFactor() : param.getWidthScalingFactor();
destinationImage =
new ScaledThumbnailMaker()
// 这里的scale方法只是传递了系数,没有其他操作
.scale(widthScalingFactor, heightScalingFactor)
.imageType(imageType)
// 从参数配置中获取缩放器工厂类
.resizerFactory(param.getResizerFactory())
// 重点-调用makeThumbnail()图片缩放及输出文件流
.make(sourceImage);
} else {
throw new IllegalStateException("Parameters to make thumbnail" +
" does not have scaling factor nor thumbnail size specified.");
}
// Perform the image filters
for (ImageFilter filter : param.getImageFilters()) {
destinationImage = filter.apply(destinationImage);
}
// Write the thumbnail image to the destination.
task.write(destinationImage);
sourceImage.flush();
destinationImage.flush();
}
通过缩放器Resizer对图片进行缩放。
/**
* Makes a thumbnail of the specified dimensions, from the specified
* source image.
*
* @param img The source image.
* @param width The target width of the thumbnail.
* @param height The target height of the thumbnail.
* @return The thumbnail image.
* @throws IllegalStateException If the {@code ThumbnailMaker} is
* not ready to create thumbnails.
* @throws IllegalArgumentException If the width and/or height is less
* than or equal to zero.
*/
protected BufferedImage makeThumbnail(BufferedImage img, int width, int height) {
if (!ready.isReady()) {
throw new IllegalStateException(ThumbnailMaker.NOT_READY_FOR_MAKE);
}
if (width <= 0) {
throw new IllegalArgumentException(
"Width must be greater than zero."
);
}
if (height <= 0) {
throw new IllegalArgumentException(
"Height must be greater than zero."
);
}
BufferedImage thumbnailImage =
new BufferedImageBuilder(width, height, imageType).build();
Dimension imgSize = new Dimension(img.getWidth(), img.getHeight());
Dimension thumbnailSize = new Dimension(width, height);
// 获取缩放器
Resizer resizer = resizerFactory.getResizer(imgSize, thumbnailSize);
// 对图片进行缩放
resizer.resize(img, thumbnailImage);
return thumbnailImage;
}
Resizer核心功能是使用Graphics2D重绘图片。
public void resize(BufferedImage srcImage, BufferedImage destImage)
throws NullPointerException {
super.performChecks(srcImage, destImage);
int currentWidth = srcImage.getWidth();
int currentHeight = srcImage.getHeight();
final int targetWidth = destImage.getWidth();
final int targetHeight = destImage.getHeight();
// If multi-step downscaling is not required, perform one-step.
if ((targetWidth * 2 >= currentWidth) && (targetHeight * 2 >= currentHeight)) {
Graphics2D g = createGraphics(destImage);
g.drawImage(srcImage, 0, 0, targetWidth, targetHeight, null);
g.dispose();
return;
}
// Temporary image used for in-place resizing of image.
BufferedImage tempImage = new BufferedImageBuilder(
currentWidth,
currentHeight,
destImage.getType()
).build();
Graphics2D g = createGraphics(tempImage);
g.setComposite(AlphaComposite.Src);
/*
* Determine the size of the first resize step should be.
* 1) Beginning from the target size
* 2) Increase each dimension by 2
* 3) Until reaching the original size
*/
int startWidth = targetWidth;
int startHeight = targetHeight;
while (startWidth < currentWidth && startHeight < currentHeight) {
startWidth *= 2;
startHeight *= 2;
}
currentWidth = startWidth / 2;
currentHeight = startHeight / 2;
// Perform first resize step.
g.drawImage(srcImage, 0, 0, currentWidth, currentHeight, null);
// Perform an in-place progressive bilinear resize.
while ( (currentWidth >= targetWidth * 2) && (currentHeight >= targetHeight * 2) ) {
currentWidth /= 2;
currentHeight /= 2;
if (currentWidth < targetWidth) {
currentWidth = targetWidth;
}
if (currentHeight < targetHeight) {
currentHeight = targetHeight;
}
g.drawImage(
tempImage,
0, 0, currentWidth, currentHeight,
0, 0, currentWidth * 2, currentHeight * 2,
null
);
}
g.dispose();
// Draw the resized image onto the destination image.
Graphics2D destg = createGraphics(destImage);
destg.drawImage(tempImage, 0, 0, targetWidth, targetHeight, 0, 0, currentWidth, currentHeight, null);
destg.dispose();
}
1.Thumbnail通过scale系数压缩,实际是通过控制长宽乘以系数达到图片大小的变化,压缩时会抛弃一些像素,所以最终压缩不一定与系数一致(实际测试发现相差很远)。
由于长宽都会乘以压缩系数,所以倒推scale时,要用destSize/originSize然后开平方。
2.Thumbnail有3种压缩算法,分别是BILINEAR,BICUBIC,PROGRESSIVE_BILINEAR。
前2个压缩算法resize()方法完全一致,属于双线性压缩。
默认压缩算法当压缩比例小于一半时,会采用PROGRESSIVE_BILINEAR。