Compare commits

...

3 Commits

Author SHA1 Message Date
Christoph Hagen
31edd35463 Print image overviews 2022-09-26 17:00:39 +02:00
Christoph Hagen
b39066f47f Fix memory leaks, sizes for image generation 2022-09-26 17:00:25 +02:00
Christoph Hagen
152a76935b Generate navigation links 2022-09-25 22:07:34 +02:00
8 changed files with 148 additions and 52 deletions

View File

@ -126,6 +126,20 @@ extension Element {
This property is mandatory at root level, and is propagated to child elements. This property is mandatory at root level, and is propagated to child elements.
*/ */
let relatedContentText: String let relatedContentText: String
/**
The text to display on the navigation element pointing to this element as the next page.
This property is mandatory at root level, and is propagated to child elements.
*/
let navigationTextAsNextPage: String
/**
The text to display on a navigation element pointing to this element as the previous page.
This property is mandatory at root level, and is propagated to child elements.
*/
let navigationTextAsPreviousPage: String
} }
} }
@ -171,6 +185,10 @@ extension Element.LocalizedMetadata {
self.externalUrl = log.unexpected(data.externalUrl, name: "externalUrl", source: source) self.externalUrl = log.unexpected(data.externalUrl, name: "externalUrl", source: source)
self.relatedContentText = log self.relatedContentText = log
.required(data.relatedContentText, name: "relatedContentText", source: source) ?? "" .required(data.relatedContentText, name: "relatedContentText", source: source) ?? ""
self.navigationTextAsNextPage = log
.required(data.navigationTextAsNextPage, name: "navigationTextAsNextPage", source: source) ?? ""
self.navigationTextAsPreviousPage = log
.required(data.navigationTextAsPreviousPage, name: "navigationTextAsPreviousPage", source: source) ?? ""
guard isComplete else { guard isComplete else {
return nil return nil
@ -207,6 +225,8 @@ extension Element.LocalizedMetadata {
self.cornerText = data.cornerText self.cornerText = data.cornerText
self.externalUrl = data.externalUrl self.externalUrl = data.externalUrl
self.relatedContentText = data.relatedContentText ?? parent.relatedContentText self.relatedContentText = data.relatedContentText ?? parent.relatedContentText
self.navigationTextAsPreviousPage = data.navigationTextAsPreviousPage ?? parent.navigationTextAsPreviousPage
self.navigationTextAsNextPage = data.navigationTextAsNextPage ?? parent.navigationTextAsNextPage
guard isComplete else { guard isComplete else {
return nil return nil

View File

@ -310,6 +310,16 @@ extension Element {
elements.filter { $0.state.isShownInOverview } elements.filter { $0.state.isShownInOverview }
} }
var linkedElements: [LinkedElement] {
let items = sortedItems
let connected = items.enumerated().map { i, element in
let previous = i+1 < items.count ? items[i+1] : nil
let next = i > 0 ? items[i-1] : nil
return (previous, element, next)
}
return connected + elements.filter { !$0.state.isShownInOverview }.map { (nil, $0, nil )}
}
/** /**
The url of the top-level section of the element. The url of the top-level section of the element.
*/ */

View File

@ -119,6 +119,20 @@ extension GenericMetadata {
This property is mandatory at root level, and is propagated to child elements. This property is mandatory at root level, and is propagated to child elements.
*/ */
let relatedContentText: String? let relatedContentText: String?
/**
The text to display on a navigation element pointing to this element as the previous page.
This property is mandatory at root level, and is propagated to child elements.
*/
let navigationTextAsPreviousPage: String?
/**
The text to display on the navigation element pointing to this element as the next page.
This property is mandatory at root level, and is propagated to child elements.
*/
let navigationTextAsNextPage: String?
} }
} }
@ -142,6 +156,8 @@ extension GenericMetadata.LocalizedMetadata: Codable {
.cornerText, .cornerText,
.externalUrl, .externalUrl,
.relatedContentText, .relatedContentText,
.navigationTextAsPreviousPage,
.navigationTextAsNextPage,
] ]
} }
@ -172,7 +188,9 @@ extension GenericMetadata.LocalizedMetadata {
thumbnailSuffix: nil, thumbnailSuffix: nil,
cornerText: nil, cornerText: nil,
externalUrl: nil, externalUrl: nil,
relatedContentText: nil) relatedContentText: nil,
navigationTextAsPreviousPage: nil,
navigationTextAsNextPage: nil)
} }
/** /**
@ -194,7 +212,9 @@ extension GenericMetadata.LocalizedMetadata {
thumbnailSuffix: nil, thumbnailSuffix: nil,
cornerText: nil, cornerText: nil,
externalUrl: nil, externalUrl: nil,
relatedContentText: "") relatedContentText: "",
navigationTextAsPreviousPage: "",
navigationTextAsNextPage: "")
} }
static var full: GenericMetadata.LocalizedMetadata { static var full: GenericMetadata.LocalizedMetadata {
@ -213,6 +233,8 @@ extension GenericMetadata.LocalizedMetadata {
thumbnailSuffix: "", thumbnailSuffix: "",
cornerText: "", cornerText: "",
externalUrl: "", externalUrl: "",
relatedContentText: "") relatedContentText: "",
navigationTextAsPreviousPage: "",
navigationTextAsNextPage: "")
} }
} }

View File

@ -23,9 +23,9 @@ enum ThumbnailStyle: String, CaseIterable {
case .large: case .large:
return 374 return 374
case .square: case .square:
return height return 178
case .small: case .small:
return height return 78
} }
} }
} }

View File

@ -7,8 +7,8 @@ extension NSImage {
guard self.size.width > size.width else { guard self.size.width > size.width else {
return self return self
} }
return NSImage(size: size, flipped: false) { (resizedRect) -> Bool in return NSImage(size: size, flipped: false) { [weak self] (resizedRect) -> Bool in
self.draw(in: resizedRect) self?.draw(in: resizedRect)
return true return true
} }
} }

View File

@ -113,6 +113,7 @@ final class ImageGenerator {
} }
func requireImage(at destination: String, generatedFrom source: String, requiredBy path: String, width: Int, height: Int?) -> NSSize { func requireImage(at destination: String, generatedFrom source: String, requiredBy path: String, width: Int, height: Int?) -> NSSize {
requiredImages.insert(destination)
let height = height.unwrapped(CGFloat.init) let height = height.unwrapped(CGFloat.init)
let sourceUrl = input.appendingPathComponent(source) let sourceUrl = input.appendingPathComponent(source)
guard sourceUrl.exists else { guard sourceUrl.exists else {
@ -158,6 +159,8 @@ final class ImageGenerator {
} }
printMissingImages() printMissingImages()
printImageWarnings() printImageWarnings()
printGeneratedImages()
printTotalImageCount()
} }
private func printMissingImages() { private func printMissingImages() {
@ -180,6 +183,20 @@ final class ImageGenerator {
} }
} }
private func printGeneratedImages() {
guard !generatedImages.isEmpty else {
return
}
print("\(generatedImages.count) images generated:")
for image in generatedImages {
print(" " + image)
}
}
private func printTotalImageCount() {
print("\(requiredImages.count) images")
}
private func addWarning(_ message: String, destination: String, path: String) { private func addWarning(_ message: String, destination: String, path: String) {
let warning = " \(destination): \(message) required by \(path)" let warning = " \(destination): \(message) required by \(path)"
imageWarnings.insert(warning) imageWarnings.insert(warning)
@ -204,14 +221,13 @@ final class ImageGenerator {
missingImages[source] = images.first?.path missingImages[source] = images.first?.path
return return
} }
if imageHasChanged { let jobs = imageHasChanged ? images : images.filter(isMissing)
// Update all images // Update all images
images.forEach { create(job: $0, from: image, source: source) } jobs.forEach { job in
} else { // Prevent memory overflow due to repeated NSImage operations
// Update only missing images autoreleasepool {
images create(job: job, from: image, source: source)
.filter(isMissing) }
.forEach { create(job: $0, from: image, source: source) }
} }
} }
@ -224,31 +240,43 @@ final class ImageGenerator {
fatalError() fatalError()
} }
let desiredWidth = CGFloat(image.size.width)
let destinationSize = image.size.scaledDown(to: desiredWidth)
let scaledImage = image.scaledDown(to: destinationSize)
let scaledSize = scaledImage.size
if abs(scaledSize.width - desiredWidth) > 2 {
addWarning("Invalid width (\(scaledSize.width) instead of \(desiredWidth))", job: job)
}
if scaledSize.width > desiredWidth {
addWarning("Invalid width (\(scaledSize.width) instead of \(desiredWidth))", job: job)
}
let destinationExtension = destinationUrl.pathExtension.lowercased() let destinationExtension = destinationUrl.pathExtension.lowercased()
guard let type = ImageType(fileExtension: destinationExtension)?.fileType else { guard let type = ImageType(fileExtension: destinationExtension)?.fileType else {
addWarning("Invalid image extension \(destinationExtension)", job: job) addWarning("Invalid image extension \(destinationExtension)", job: job)
return return
} }
guard let tiff = scaledImage.tiffRepresentation, let tiffData = NSBitmapImageRep(data: tiff) else {
addWarning("Failed to get data", job: job)
return
}
guard let data = tiffData.representation(using: type, properties: [.compressionFactor: NSNumber(0.7)]) else { let desiredWidth = CGFloat(job.width)
let sourceRep = image.representations[0]
let destinationSize = NSSize(width: sourceRep.pixelsWide, height: sourceRep.pixelsHigh)
.scaledDown(to: desiredWidth)
//image.size.scaledDown(to: desiredWidth)
print("\(job.destination):")
print(" Source: \(image.size.width) x \(image.size.height)")
print(" Wanted: \(destinationSize.width) x \(destinationSize.height) (\(job.width))")
// create NSBitmapRep manually, if using cgImage, the resulting size is wrong
let rep = NSBitmapImageRep(bitmapDataPlanes: nil,
pixelsWide: Int(destinationSize.width),
pixelsHigh: Int(destinationSize.height),
bitsPerSample: 8,
samplesPerPixel: 4,
hasAlpha: true,
isPlanar: false,
colorSpaceName: NSColorSpaceName.deviceRGB,
bytesPerRow: Int(destinationSize.width) * 4,
bitsPerPixel: 32)!
let ctx = NSGraphicsContext(bitmapImageRep: rep)
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = ctx
image.draw(in: NSMakeRect(0, 0, destinationSize.width, destinationSize.height))
ctx?.flushGraphics()
NSGraphicsContext.restoreGraphicsState()
// Get NSData, and save it
guard let data = rep.representation(using: type, properties: [.compressionFactor: NSNumber(0.7)]) else {
addWarning("Failed to get data", job: job) addWarning("Failed to get data", job: job)
return return
} }
@ -258,5 +286,6 @@ final class ImageGenerator {
addWarning("Failed to write image (\(error))", job: job) addWarning("Failed to write image (\(error))", job: job)
return return
} }
generatedImages.insert(job.destination)
} }
} }

View File

@ -3,20 +3,13 @@ import Ink
struct PageGenerator { struct PageGenerator {
struct NavigationLink {
let link: String
let text: String
}
private let factory: LocalizedSiteTemplate private let factory: LocalizedSiteTemplate
init(factory: LocalizedSiteTemplate) { init(factory: LocalizedSiteTemplate) {
self.factory = factory self.factory = factory
} }
func generate(page: Element, language: String, nextPage: NavigationLink?, previousPage: NavigationLink?) { func generate(page: Element, language: String, previousPage: Element?, nextPage: Element?) {
guard !page.isExternalPage else { guard !page.isExternalPage else {
return return
} }
@ -35,10 +28,10 @@ struct PageGenerator {
content[.header] = makeHeader(page: page, metadata: metadata, language: language) content[.header] = makeHeader(page: page, metadata: metadata, language: language)
content[.content] = pageContent content[.content] = pageContent
content[.previousPageLinkText] = previousPage.unwrapped { factory.factory.html.makePrevText($0.text) } content[.previousPageLinkText] = previousText(for: previousPage, language: language)
content[.previousPageUrl] = previousPage?.link content[.previousPageUrl] = navLink(from: page, to: previousPage, language: language)
content[.nextPageLinkText] = nextPage.unwrapped { factory.factory.html.makeNextText($0.text) } content[.nextPageLinkText] = nextText(for: nextPage, language: language)
content[.nextPageUrl] = nextPage?.link content[.nextPageUrl] = navLink(from: page, to: nextPage, language: language)
content[.footer] = page.customFooterContent() content[.footer] = page.customFooterContent()
if pageIncludesCode { if pageIncludesCode {
@ -58,6 +51,27 @@ struct PageGenerator {
files.generated(page: path) files.generated(page: path)
} }
private func navLink(from element: Element, to destination: Element?, language: String) -> String? {
guard let fullPath = destination?.fullPageUrl(for: language) else {
return nil
}
return element.relativePathToOtherSiteElement(file: fullPath)
}
private func previousText(for element: Element?, language: String) -> String? {
guard let text = element?.localized(for: language).navigationTextAsPreviousPage else {
return nil
}
return factory.factory.html.makePrevText(text)
}
private func nextText(for element: Element?, language: String) -> String? {
guard let text = element?.localized(for: language).navigationTextAsNextPage else {
return nil
}
return factory.factory.html.makeNextText(text)
}
private func makeContent(page: Element, metadata: Element.LocalizedMetadata, language: String, path: String) -> (content: String, includesCode: Bool, isEmpty: Bool) { private func makeContent(page: Element, metadata: Element.LocalizedMetadata, language: String, path: String) -> (content: String, includesCode: Bool, isEmpty: Bool) {
let create = configuration.createMdFilesIfMissing let create = configuration.createMdFilesIfMissing
if let raw = files.contentOfOptionalFile(atPath: path, source: page.path, createEmptyFileIfMissing: create)? if let raw = files.contentOfOptionalFile(atPath: path, source: page.path, createEmptyFileIfMissing: create)?

View File

@ -1,5 +1,7 @@
import Foundation import Foundation
typealias LinkedElement = (previous: Element?, element: Element, next: Element?)
struct SiteGenerator { struct SiteGenerator {
let templates: TemplateFactory let templates: TemplateFactory
@ -26,22 +28,21 @@ struct SiteGenerator {
let overviewGenerator = OverviewPageGenerator(factory: template) let overviewGenerator = OverviewPageGenerator(factory: template)
let pageGenerator = PageGenerator(factory: template) let pageGenerator = PageGenerator(factory: template)
var elementsToProcess: [Element] = [site] var elementsToProcess: [LinkedElement] = [(nil, site, nil)]
while let element = elementsToProcess.popLast() { while let (previous, element, next) = elementsToProcess.popLast() {
// Move recursively down to all pages // Move recursively down to all pages
elementsToProcess.append(contentsOf: element.elements) elementsToProcess.append(contentsOf: element.linkedElements)
processAllFiles(for: element) processAllFiles(for: element)
if !element.elements.isEmpty { if !element.elements.isEmpty {
overviewGenerator.generate(section: element, language: language) overviewGenerator.generate(section: element, language: language)
} else { } else {
#warning("Determine previous and next pages (with relative links)")
pageGenerator.generate( pageGenerator.generate(
page: element, page: element,
language: language, language: language,
nextPage: nil, previousPage: previous,
previousPage: nil) nextPage: next)
} }
} }
} }