Support GIFs

This commit is contained in:
Christoph Hagen 2025-01-05 21:32:25 +01:00
parent ac7fbdd638
commit c78c359819
9 changed files with 108 additions and 81 deletions

View File

@ -127,7 +127,7 @@
E29D318B2D0B07EE0051B7F4 /* ContentBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318A2D0B07E60051B7F4 /* ContentBox.swift */; };
E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */; };
E29D31902D0B34870051B7F4 /* GenerationAnomaly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */; };
E29D31942D0B7D280051B7F4 /* SvgImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31932D0B7D250051B7F4 /* SvgImage.swift */; };
E29D31942D0B7D280051B7F4 /* SimpleImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31932D0B7D250051B7F4 /* SimpleImage.swift */; };
E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31952D0C18690051B7F4 /* PathSettings.swift */; };
E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */; };
E29D319B2D0C452B0051B7F4 /* PageIssue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D319A2D0C452B0051B7F4 /* PageIssue.swift */; };
@ -217,6 +217,8 @@
E2FE0F262D2AF9B0002963B7 /* ImageCompareCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F252D2AF9AA002963B7 /* ImageCompareCommand.swift */; };
E2FE0F282D2AFB11002963B7 /* ImageCompare.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F272D2AFB0A002963B7 /* ImageCompare.swift */; };
E2FE0F2A2D2AFBE6002963B7 /* ImageCompareIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F292D2AFBE3002963B7 /* ImageCompareIcons.swift */; };
E2FE0F2C2D2B119A002963B7 /* ImageCommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F2B2D2B1196002963B7 /* ImageCommandProcessor.swift */; };
E2FE0F312D2B1952002963B7 /* PartialSvgImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F302D2B1952002963B7 /* PartialSvgImage.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -336,7 +338,7 @@
E29D318A2D0B07E60051B7F4 /* ContentBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBox.swift; sourceTree = "<group>"; };
E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsContentView.swift; sourceTree = "<group>"; };
E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationAnomaly.swift; sourceTree = "<group>"; };
E29D31932D0B7D250051B7F4 /* SvgImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SvgImage.swift; sourceTree = "<group>"; };
E29D31932D0B7D250051B7F4 /* SimpleImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleImage.swift; sourceTree = "<group>"; };
E29D31952D0C18690051B7F4 /* PathSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettings.swift; sourceTree = "<group>"; };
E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettingsFile.swift; sourceTree = "<group>"; };
E29D319A2D0C452B0051B7F4 /* PageIssue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIssue.swift; sourceTree = "<group>"; };
@ -425,6 +427,8 @@
E2FE0F252D2AF9AA002963B7 /* ImageCompareCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompareCommand.swift; sourceTree = "<group>"; };
E2FE0F272D2AFB0A002963B7 /* ImageCompare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompare.swift; sourceTree = "<group>"; };
E2FE0F292D2AFBE3002963B7 /* ImageCompareIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompareIcons.swift; sourceTree = "<group>"; };
E2FE0F2B2D2B1196002963B7 /* ImageCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCommandProcessor.swift; sourceTree = "<group>"; };
E2FE0F302D2B1952002963B7 /* PartialSvgImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialSvgImage.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -526,7 +530,7 @@
children = (
E29D31C12D0DBED70051B7F4 /* AudioPlayer */,
E29D31AB2D0DA52C0051B7F4 /* Icons */,
E29D31932D0B7D250051B7F4 /* SvgImage.swift */,
E2FE0F2F2D2B18B0002963B7 /* Images */,
E29D318A2D0B07E60051B7F4 /* ContentBox.swift */,
E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */,
E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */,
@ -590,6 +594,7 @@
E29D31B62D0DAC030051B7F4 /* Page Content */ = {
isa = PBXGroup;
children = (
E2FE0F2B2D2B1196002963B7 /* ImageCommandProcessor.swift */,
E2FE0F252D2AF9AA002963B7 /* ImageCompareCommand.swift */,
E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */,
E2FE0F142D269188002963B7 /* PageHtmlProcessor.swift */,
@ -748,14 +753,12 @@
isa = PBXGroup;
children = (
E29D311E2D0320D90051B7F4 /* ContentElements */,
E25DA58A2D020C9200AEF16D /* PageImage.swift */,
E25DA51E2CFF15C100AEF16D /* NavigationBar.swift */,
E2FE0EF92D25AFB5002963B7 /* PageHeader.swift */,
E25DA51A2CFF08AF00AEF16D /* PostFeedPageNavigation.swift */,
E2B85F422C4294F60047CD0C /* FeedEntry.swift */,
E2A21C452CBAE2E50060935B /* FeedEntryContent.swift */,
E2A21C272CB29B290060935B /* FeedEntryData.swift */,
E2B85F442C429ED60047CD0C /* ImageGallery.swift */,
E2B85F402C4294790047CD0C /* PageHead.swift */,
E25DA51C2CFF135B00AEF16D /* GenericPage.swift */,
);
@ -880,6 +883,17 @@
path = "Page Generators";
sourceTree = "<group>";
};
E2FE0F2F2D2B18B0002963B7 /* Images */ = {
isa = PBXGroup;
children = (
E2FE0F302D2B1952002963B7 /* PartialSvgImage.swift */,
E2B85F442C429ED60047CD0C /* ImageGallery.swift */,
E25DA58A2D020C9200AEF16D /* PageImage.swift */,
E29D31932D0B7D250051B7F4 /* SimpleImage.swift */,
);
path = Images;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -1015,7 +1029,7 @@
E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */,
E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */,
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,
E29D31942D0B7D280051B7F4 /* SvgImage.swift in Sources */,
E29D31942D0B7D280051B7F4 /* SimpleImage.swift in Sources */,
E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */,
E22990462D10B7A7009F8D77 /* SecurityScopeStatus.swift in Sources */,
E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */,
@ -1124,6 +1138,7 @@
E29D31C32D0DBEF20051B7F4 /* Song.swift in Sources */,
E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */,
E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */,
E2FE0F2C2D2B119A002963B7 /* ImageCommandProcessor.swift in Sources */,
E2FE0F112D268E7E002963B7 /* PageCodeProcessor.swift in Sources */,
E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */,
E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */,
@ -1162,6 +1177,7 @@
E2FE0F192D2723E3002963B7 /* ImageSet.swift in Sources */,
E2A21C362CB9A3D70060935B /* PathSettingsView.swift in Sources */,
E29D31362D0435430051B7F4 /* TabSelection.swift in Sources */,
E2FE0F312D2B1952002963B7 /* PartialSvgImage.swift in Sources */,
E2FE0F262D2AF9B0002963B7 /* ImageCompareCommand.swift in Sources */,
E2FE0F1E2D281AE1002963B7 /* TagOverviewGenerator.swift in Sources */,
E29D31572D06D38B0051B7F4 /* AddTagView.swift in Sources */,

View File

@ -0,0 +1,65 @@
struct ImageCommandProcessor: CommandProcessor {
let commandType: ShorthandMarkdownKey = .image
let content: Content
let results: PageGenerationResults
let language: ContentLanguage
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.content = content
self.results = results
self.language = language
}
private var thumbnailWidth: Int {
content.settings.pages.contentWidth
}
private var largeImageWidth: Int {
content.settings.pages.largeImageWidth
}
/**
Format: `![image](<imageId>;<caption?>]`
*/
func process(_ arguments: [String], markdown: Substring) -> String {
guard (1...2).contains(arguments.count) else {
results.invalid(command: .image, markdown)
return ""
}
let imageId = arguments[0]
guard let image = content.image(imageId) else {
results.missing(file: imageId, source: "Image command")
return ""
}
results.used(file: image)
if image.type == .svg || image.type == .gif {
return simple(image: image)
}
let thumbnail = image.imageSet(width: thumbnailWidth, height: thumbnailWidth, language: language)
results.require(imageSet: thumbnail)
let largeImage = image.imageSet(width: largeImageWidth, height: largeImageWidth, language: language)
results.require(imageSet: largeImage)
let caption = arguments.count == 2 ? arguments[1] : nil
return PageImage(
imageId: imageId.replacingOccurrences(of: ".", with: "-"),
thumbnail: thumbnail,
largeImage: largeImage,
caption: caption).content
}
private func simple(image: FileResource) -> String {
results.require(file: image)
let path = image.absoluteUrl
let altText = image.localized(in: language)
return SimpleImage(imagePath: path, altText: altText).content
}
}

View File

@ -27,20 +27,14 @@ final class PageContentParser {
private let imageCompare: ImageCompareCommandProcessor
private let images: ImageCommandProcessor
// MARK: Other handlers
private let inlineLink: InlineLinkProcessor
private let code: PageCodeProcessor
var largeImageWidth: Int {
content.settings.pages.largeImageWidth
}
var thumbnailWidth: Int {
content.settings.pages.contentWidth
}
init(content: Content, language: ContentLanguage, results: PageGenerationResults) {
self.content = content
self.results = results
@ -53,6 +47,7 @@ final class PageContentParser {
self.html = .init(content: content, results: results, language: language)
self.video = .init(content: content, results: results, language: language)
self.imageCompare = .init(content: content, results: results, language: language)
self.images = .init(content: content, results: results, language: language)
self.inlineLink = .init(content: content, results: results, language: language)
self.code = .init(results: results)
@ -105,7 +100,7 @@ final class PageContentParser {
let rawCommand = percentDecoded(markdown.between("![", and: "]").trimmed)
guard rawCommand != "" else {
return handleImage(arguments, markdown: markdown)
return images.process(arguments, markdown: markdown)
}
guard let command = ShorthandMarkdownKey(rawValue: rawCommand) else {
@ -116,7 +111,7 @@ final class PageContentParser {
switch command {
case .image:
return handleImage(arguments, markdown: markdown)
return images.process(arguments, markdown: markdown)
case .labels:
return labelHandler.process(arguments, markdown: markdown)
case .buttons:
@ -144,42 +139,6 @@ final class PageContentParser {
}
}
/**
Format: `![image](<imageId>;<caption?>]`
*/
private func handleImage(_ arguments: [String], markdown: Substring) -> String {
guard (1...2).contains(arguments.count) else {
results.invalid(command: .image, markdown)
return ""
}
let imageId = arguments[0]
guard let image = content.image(imageId) else {
results.missing(file: imageId, source: "Image command")
return ""
}
results.used(file: image)
guard !image.type.isSvg else {
let path = image.absoluteUrl
let altText = image.localized(in: language)
return SvgImage(imagePath: path, altText: altText).content
}
let thumbnail = image.imageSet(width: thumbnailWidth, height: thumbnailWidth, language: language)
results.require(imageSet: thumbnail)
let largeImage = image.imageSet(width: largeImageWidth, height: largeImageWidth, language: language)
results.require(imageSet: largeImage)
let caption = arguments.count == 2 ? arguments[1] : nil
return PageImage(
imageId: imageId.replacingOccurrences(of: ".", with: "-"),
thumbnail: thumbnail,
largeImage: largeImage,
caption: caption).content
}
/**
Format: `![page](<pageId>)`
*/
@ -298,7 +257,7 @@ final class PageContentParser {
results.missing(file: imageId, source: "SVG command")
return ""
}
guard image.type.isSvg else {
guard image.type == .svg else {
results.invalid(command: .svg, markdown)
return ""
}
@ -313,17 +272,3 @@ final class PageContentParser {
.content
}
}
/*
private func handleGif(file: String, altText: String) -> String {
let imagePath = page.pathRelativeToRootForContainedInputFile(file)
results.require(file: imagePath, source: page.path)
guard let size = results.getImageSize(atPath: imagePath, source: page.path) else {
return ""
}
let width = Int(size.width)
let height = Int(size.height)
return factory.html.image(file: file, width: width, height: height, altText: altText)
}
*/

View File

@ -15,7 +15,6 @@ import SFSafeSymbols
- Images: Show list of generated versions
**Features**
- GIF Support (No image set, don't rescale)
- Files: Optional Property `customFilePath` for external files to place them in another location
- Files: Property `version` and `sourceUrl` to track asset files
- Posts: Generate separate pages for posts to link to

View File

@ -42,6 +42,7 @@ final class PageSettings: ObservableObject {
self.audioPlayerCssFile = file.audioPlayerCssFile.map { files[$0] }
self.modelViewerJsFile = file.modelViewerJsFile.map { files[$0] }
self.imageCompareCssFile = file.imageCompareCssFile.map { files[$0] }
self.imageCompareJsFile = file.imageCompareJsFile.map { files[$0] }
}
var file: PageSettingsFile {

View File

@ -26,16 +26,3 @@ struct PartialSvgImage: HtmlProducer {
result += "</span>"
}
}
struct SvgImage: HtmlProducer {
let imagePath: String
let altText: String
func populate(_ result: inout String) {
result += "<div class='content-image svg-image'>"
result += "<img src='\(imagePath)' loading='lazy' alt='\(altText)'/>"
result += "</div>"
}
}

View File

@ -0,0 +1,14 @@
struct SimpleImage: HtmlProducer {
let imagePath: String
let altText: String
func populate(_ result: inout String) {
result += "<div class='content-image svg-image'>"
result += "<img src='\(imagePath)' loading='lazy' alt='\(altText)'/>"
result += "</div>"
}
}