mirror of https://github.com/halo-dev/halo
Add thumbnail generation router for image uploads
parent
b1127e3c28
commit
503e9b8c7f
|
@ -0,0 +1,156 @@
|
||||||
|
package run.halo.app.core.attachment.endpoint;
|
||||||
|
|
||||||
|
import static java.nio.file.StandardOpenOption.CREATE;
|
||||||
|
import static java.nio.file.StandardOpenOption.READ;
|
||||||
|
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
|
||||||
|
import static java.nio.file.StandardOpenOption.WRITE;
|
||||||
|
import static org.imgscalr.Scalr.Method.AUTOMATIC;
|
||||||
|
import static org.imgscalr.Scalr.Mode.FIT_TO_WIDTH;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.imgscalr.Scalr;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import org.springframework.web.reactive.resource.NoResourceFoundException;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Scheduler;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
import run.halo.app.core.attachment.AttachmentRootGetter;
|
||||||
|
import run.halo.app.core.attachment.ThumbnailSize;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
class ThumbnailRouters {
|
||||||
|
|
||||||
|
private final Path uploadRoot;
|
||||||
|
|
||||||
|
private final Path thumbnailRoot;
|
||||||
|
|
||||||
|
private final Scheduler thumbnailGeneratingScheduler;
|
||||||
|
|
||||||
|
public ThumbnailRouters(AttachmentRootGetter attachmentRoot) {
|
||||||
|
this.uploadRoot = attachmentRoot.get().resolve("upload");
|
||||||
|
this.thumbnailRoot = attachmentRoot.get().resolve("thumbnails");
|
||||||
|
this.thumbnailGeneratingScheduler = Schedulers.newBoundedElastic(
|
||||||
|
// Currently, we only allow 10 concurrent thumbnail generations
|
||||||
|
10,
|
||||||
|
Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE,
|
||||||
|
"thumbnail-generator-"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
RouterFunction<ServerResponse> thumbnailRouter() {
|
||||||
|
return RouterFunctions.route()
|
||||||
|
.GET("/thumbnails/w{width}/upload/{*filename}", serverRequest -> {
|
||||||
|
var width = serverRequest.pathVariable("width");
|
||||||
|
String originalFilename = serverRequest.pathVariable("filename");
|
||||||
|
var fileName = StringUtils.removeStart(originalFilename, "/");
|
||||||
|
if (StringUtils.isBlank(fileName)) {
|
||||||
|
log.trace("Filename is blank");
|
||||||
|
return Mono.error(new NoResourceFoundException(fileName));
|
||||||
|
}
|
||||||
|
var size = ThumbnailSize.fromWidth(width);
|
||||||
|
// try to resolve the thumbnail
|
||||||
|
// build thumbnail path
|
||||||
|
var thumbnailPath = thumbnailRoot.resolve("w" + size.getWidth())
|
||||||
|
.resolve(fileName);
|
||||||
|
var thumbnailResource = new FileSystemResource(thumbnailPath);
|
||||||
|
if (thumbnailResource.isReadable()) {
|
||||||
|
return ServerResponse.ok().bodyValue(thumbnailResource);
|
||||||
|
}
|
||||||
|
// generate for the attachment
|
||||||
|
return Mono.fromCallable(
|
||||||
|
() -> {
|
||||||
|
var uploadPath = uploadRoot.resolve(fileName);
|
||||||
|
return generateThumbnail(uploadPath, thumbnailPath, size);
|
||||||
|
})
|
||||||
|
.subscribeOn(this.thumbnailGeneratingScheduler)
|
||||||
|
.switchIfEmpty(Mono.error(() -> new NoResourceFoundException(fileName)))
|
||||||
|
.map(FileSystemResource::new)
|
||||||
|
.flatMap(resource -> ServerResponse.ok().bodyValue(resource));
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path generateThumbnail(Path attachmentPath, Path thumbnailPath, ThumbnailSize size) {
|
||||||
|
if (!Files.exists(attachmentPath)) {
|
||||||
|
log.trace("Attachment path does not exist: {}", attachmentPath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (Files.exists(thumbnailPath)) {
|
||||||
|
return thumbnailPath;
|
||||||
|
}
|
||||||
|
var attachmentPathString = attachmentPath.toString();
|
||||||
|
synchronized (attachmentPathString) {
|
||||||
|
// double check
|
||||||
|
if (Files.exists(thumbnailPath)) {
|
||||||
|
return thumbnailPath;
|
||||||
|
}
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Generating thumbnail for path: {}, target: {}, size: {}",
|
||||||
|
attachmentPath, thumbnailPath, size);
|
||||||
|
}
|
||||||
|
var formatName = getFormatName(attachmentPath);
|
||||||
|
if (formatName == null) {
|
||||||
|
log.info("Cannot determine image format for path: {}", attachmentPath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try (
|
||||||
|
var inputStream = Files.newInputStream(attachmentPath, READ);
|
||||||
|
) {
|
||||||
|
var bufferedImage = ImageIO.read(inputStream);
|
||||||
|
if (bufferedImage == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (bufferedImage.getWidth() <= size.getWidth()) {
|
||||||
|
// if the image is smaller than the thumbnail size, just copy it
|
||||||
|
Files.createDirectories(thumbnailPath.getParent());
|
||||||
|
Files.copy(attachmentPath, thumbnailPath);
|
||||||
|
return thumbnailPath;
|
||||||
|
}
|
||||||
|
// TODO Handle image orientation
|
||||||
|
var thumbnailBufferedImage =
|
||||||
|
Scalr.resize(bufferedImage, AUTOMATIC, FIT_TO_WIDTH, size.getWidth());
|
||||||
|
Files.createDirectories(thumbnailPath.getParent());
|
||||||
|
try (var outputStream =
|
||||||
|
Files.newOutputStream(thumbnailPath, CREATE, TRUNCATE_EXISTING, WRITE)
|
||||||
|
) {
|
||||||
|
ImageIO.write(thumbnailBufferedImage, formatName, outputStream);
|
||||||
|
}
|
||||||
|
log.info("Generated thumbnail for path: {}, target: {}, size: {}",
|
||||||
|
attachmentPath, thumbnailPath, size);
|
||||||
|
return thumbnailPath;
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to generate thumbnail for path: {}", attachmentPath, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFormatName(Path imagePath) {
|
||||||
|
try (var imageInputStream = ImageIO.createImageInputStream(
|
||||||
|
Files.newInputStream(imagePath))
|
||||||
|
) {
|
||||||
|
var readers = ImageIO.getImageReaders(imageInputStream);
|
||||||
|
if (!readers.hasNext()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var reader = readers.next();
|
||||||
|
return reader.getFormatName();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to get image format for path: {}", imagePath, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue