-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement new mechanisms for generating and managing attachment…
… thumbnails
- Loading branch information
Showing
38 changed files
with
2,211 additions
and
113 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
api/src/main/java/run/halo/app/core/attachment/ThumbnailProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package run.halo.app.core.attachment; | ||
|
||
import java.net.URI; | ||
import java.net.URL; | ||
import lombok.Builder; | ||
import lombok.Data; | ||
import org.pf4j.ExtensionPoint; | ||
import reactor.core.publisher.Mono; | ||
|
||
public interface ThumbnailProvider extends ExtensionPoint { | ||
|
||
/** | ||
* Generate thumbnail URI for given image URL and size. | ||
* | ||
* @param context Thumbnail context including image URI and size | ||
* @return Generated thumbnail URI | ||
*/ | ||
Mono<URI> generate(ThumbnailContext context); | ||
|
||
/** | ||
* Delete thumbnail file for given image URL. | ||
* | ||
* @param imageUrl original image URL | ||
*/ | ||
Mono<Void> delete(URL imageUrl); | ||
|
||
/** | ||
* Whether the provider supports the given image URI. | ||
* | ||
* @return {@code true} if supports, {@code false} otherwise | ||
*/ | ||
Mono<Boolean> supports(ThumbnailContext context); | ||
|
||
@Data | ||
@Builder | ||
class ThumbnailContext { | ||
private final URL imageUrl; | ||
private final ThumbnailSize size; | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
api/src/main/java/run/halo/app/core/attachment/ThumbnailSize.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package run.halo.app.core.attachment; | ||
|
||
import lombok.Getter; | ||
|
||
@Getter | ||
public enum ThumbnailSize { | ||
S(400), | ||
M(800), | ||
L(1200), | ||
XL(1600); | ||
|
||
private final int width; | ||
|
||
ThumbnailSize(int width) { | ||
this.width = width; | ||
} | ||
|
||
/** | ||
* Convert width string to {@link ThumbnailSize}. | ||
* | ||
* @param width width string | ||
*/ | ||
public static ThumbnailSize fromWidth(String width) { | ||
for (ThumbnailSize value : values()) { | ||
if (String.valueOf(value.getWidth()).equals(width)) { | ||
return value; | ||
} | ||
} | ||
return ThumbnailSize.M; | ||
} | ||
|
||
/** | ||
* Convert name to {@link ThumbnailSize}. | ||
*/ | ||
public static ThumbnailSize fromName(String name) { | ||
for (ThumbnailSize value : values()) { | ||
if (value.name().equalsIgnoreCase(name)) { | ||
return value; | ||
} | ||
} | ||
throw new IllegalArgumentException("No such thumbnail size: " + name); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
application/src/main/java/run/halo/app/content/HtmlThumbnailSrcsetInjector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package run.halo.app.content; | ||
|
||
import java.net.URI; | ||
import java.util.function.Function; | ||
import lombok.experimental.UtilityClass; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.jsoup.Jsoup; | ||
import org.jsoup.nodes.Document; | ||
import org.jsoup.select.Elements; | ||
import reactor.core.publisher.Flux; | ||
import reactor.core.publisher.Mono; | ||
import run.halo.app.core.attachment.ThumbnailService; | ||
import run.halo.app.core.attachment.ThumbnailSize; | ||
|
||
@UtilityClass | ||
public class HtmlThumbnailSrcsetInjector { | ||
static final String SRC = "src"; | ||
static final String SRCSET = "srcset"; | ||
|
||
/** | ||
* Inject srcset attribute to img tags in the given html. | ||
*/ | ||
public static Mono<String> injectSrcset(String html, | ||
Function<String, Mono<String>> srcSetValueGenerator) { | ||
Document document = Jsoup.parseBodyFragment(html); | ||
document.outputSettings(new Document.OutputSettings().prettyPrint(false)); | ||
|
||
Elements imgTags = document.select("img[src]"); | ||
return Flux.fromIterable(imgTags) | ||
.filter(element -> { | ||
String src = element.attr(SRC); | ||
return !element.hasAttr(SRCSET) && isValidSrc(src); | ||
}) | ||
.flatMap(img -> { | ||
String src = img.attr(SRC); | ||
return srcSetValueGenerator.apply(src) | ||
.filter(StringUtils::isNotBlank) | ||
.doOnNext(srcsetValue -> { | ||
img.attr(SRCSET, srcsetValue); | ||
img.attr("sizes", buildSizesAttr()); | ||
}); | ||
}) | ||
.then(Mono.fromSupplier(() -> document.body().html())); | ||
} | ||
|
||
static String buildSizesAttr() { | ||
var sb = new StringBuilder(); | ||
var delimiter = ", "; | ||
var sizes = ThumbnailSize.values(); | ||
for (int i = 0; i < sizes.length; i++) { | ||
var size = sizes[i]; | ||
sb.append("(max-width: ").append(size.getWidth()).append("px)") | ||
.append(" ") | ||
.append(size.getWidth()) | ||
.append("px"); | ||
if (i < sizes.length - 1) { | ||
sb.append(delimiter); | ||
} | ||
} | ||
return sb.toString(); | ||
} | ||
|
||
/** | ||
* Generate srcset attribute value for the given src. | ||
*/ | ||
public static Mono<String> generateSrcset(URI src, ThumbnailService thumbnailService) { | ||
return Flux.fromArray(ThumbnailSize.values()) | ||
.flatMap(size -> thumbnailService.generate(src, size) | ||
.map(thumbnail -> thumbnail.toString() + " " + size.getWidth() + "w") | ||
) | ||
.collect(StringBuilder::new, (builder, srcsetValue) -> { | ||
if (!builder.isEmpty()) { | ||
builder.append(", "); | ||
} | ||
builder.append(srcsetValue); | ||
}) | ||
.map(StringBuilder::toString); | ||
} | ||
|
||
private static boolean isValidSrc(String src) { | ||
if (StringUtils.isBlank(src)) { | ||
return false; | ||
} | ||
try { | ||
URI.create(src); | ||
return true; | ||
} catch (IllegalArgumentException e) { | ||
// ignore | ||
} | ||
return false; | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
application/src/main/java/run/halo/app/content/PostContentThumbnailHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package run.halo.app.content; | ||
|
||
import static run.halo.app.content.HtmlThumbnailSrcsetInjector.generateSrcset; | ||
|
||
import java.net.URI; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.lang.NonNull; | ||
import org.springframework.stereotype.Component; | ||
import reactor.core.publisher.Mono; | ||
import run.halo.app.core.attachment.ThumbnailService; | ||
import run.halo.app.theme.ReactivePostContentHandler; | ||
|
||
/** | ||
* A post content handler to handle post html content and generate thumbnail by the img tag. | ||
* | ||
* @author guqing | ||
* @since 2.19.0 | ||
*/ | ||
@Slf4j | ||
@Component | ||
@RequiredArgsConstructor | ||
public class PostContentThumbnailHandler implements ReactivePostContentHandler { | ||
private final ThumbnailService thumbnailService; | ||
|
||
@Override | ||
public Mono<PostContentContext> handle(@NonNull PostContentContext postContent) { | ||
var html = postContent.getContent(); | ||
return HtmlThumbnailSrcsetInjector.injectSrcset(html, | ||
src -> generateSrcset(URI.create(src), thumbnailService) | ||
) | ||
.onErrorResume(throwable -> { | ||
log.debug("Failed to inject srcset to post content, fallback to original content", | ||
throwable); | ||
return Mono.just(html); | ||
}) | ||
.doOnNext(postContent::setContent) | ||
.thenReturn(postContent); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
application/src/main/java/run/halo/app/core/attachment/AttachmentRootGetter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package run.halo.app.core.attachment; | ||
|
||
import java.nio.file.Path; | ||
import java.util.function.Supplier; | ||
|
||
/** | ||
* Gets the root path(work dir) of the local attachment. | ||
*/ | ||
public interface AttachmentRootGetter extends Supplier<Path> { | ||
} |
56 changes: 56 additions & 0 deletions
56
application/src/main/java/run/halo/app/core/attachment/AttachmentUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package run.halo.app.core.attachment; | ||
|
||
import static run.halo.app.infra.FileCategoryMatcher.IMAGE; | ||
|
||
import java.net.MalformedURLException; | ||
import java.net.URI; | ||
import java.net.URL; | ||
import java.nio.charset.StandardCharsets; | ||
import lombok.experimental.UtilityClass; | ||
import org.springframework.lang.NonNull; | ||
import org.springframework.util.Assert; | ||
import org.springframework.web.util.UriComponentsBuilder; | ||
import org.springframework.web.util.UriUtils; | ||
import run.halo.app.core.extension.attachment.Attachment; | ||
|
||
@UtilityClass | ||
public class AttachmentUtils { | ||
/** | ||
* Check whether the attachment is an image. | ||
* | ||
* @param attachment Attachment must not be null | ||
* @return true if the attachment is an image, false otherwise | ||
*/ | ||
public static boolean isImage(Attachment attachment) { | ||
Assert.notNull(attachment, "Attachment must not be null"); | ||
var mediaType = attachment.getSpec().getMediaType(); | ||
return mediaType != null && IMAGE.match(mediaType); | ||
} | ||
|
||
/** | ||
* Convert URI to URL. | ||
* | ||
* @param uri URI must not be null | ||
* @return URL | ||
* @throws IllegalArgumentException if the URL is malformed | ||
*/ | ||
public static URL toUrl(@NonNull URI uri) { | ||
try { | ||
return uri.toURL(); | ||
} catch (MalformedURLException e) { | ||
throw new IllegalArgumentException(e); | ||
} | ||
} | ||
|
||
/** | ||
* Encode uri string to URI. | ||
* This method will decode the uri string first and then encode it. | ||
*/ | ||
public static URI encodeUri(String uriStr) { | ||
var decodedUriStr = UriUtils.decode(uriStr, StandardCharsets.UTF_8); | ||
return UriComponentsBuilder.fromUriString(decodedUriStr) | ||
.encode(StandardCharsets.UTF_8) | ||
.build() | ||
.toUri(); | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
application/src/main/java/run/halo/app/core/attachment/LocalThumbnailProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package run.halo.app.core.attachment; | ||
|
||
import java.net.URI; | ||
import java.net.URL; | ||
import lombok.RequiredArgsConstructor; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.util.Assert; | ||
import reactor.core.publisher.Mono; | ||
import run.halo.app.infra.ExternalUrlSupplier; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class LocalThumbnailProvider implements ThumbnailProvider { | ||
private final ExternalUrlSupplier externalUrlSupplier; | ||
private final LocalThumbnailService localThumbnailService; | ||
|
||
@Override | ||
public Mono<URI> generate(ThumbnailContext context) { | ||
return localThumbnailService.create(context.getImageUrl(), context.getSize()) | ||
.map(localThumbnail -> localThumbnail.getSpec().getThumbnailUri()) | ||
.map(URI::create); | ||
} | ||
|
||
@Override | ||
public Mono<Void> delete(URL imageUrl) { | ||
Assert.notNull(imageUrl, "Image URL must not be null"); | ||
return localThumbnailService.delete(URI.create(imageUrl.toString())); | ||
} | ||
|
||
@Override | ||
public Mono<Boolean> supports(ThumbnailContext context) { | ||
var imageUrl = context.getImageUrl(); | ||
var externalUrl = externalUrlSupplier.getRaw(); | ||
return Mono.fromSupplier(() -> externalUrl != null | ||
&& isSameOrigin(imageUrl, externalUrl)); | ||
} | ||
|
||
private boolean isSameOrigin(URL imageUrl, URL externalUrl) { | ||
return StringUtils.equals(imageUrl.getHost(), externalUrl.getHost()) | ||
&& imageUrl.getPort() == externalUrl.getPort(); | ||
} | ||
} |
Oops, something went wrong.