Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/.build
/.local
/.swiftpm
/.vscode
/_site
/archives
/build
Expand Down
18 changes: 18 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 40 additions & 16 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,44 @@

import PackageDescription

#if os(macOS)

let coreDependencies: [Target.Dependency] = [
"PlatformSupport",
"Hoedown",
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "FSEventsWrapper", package: "FSEventsWrapper"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdFoundation", package: "hummingbird"),
.product(name: "Licensable", package: "licensable"),
.product(name: "Logging", package: "swift-log"),
.product(name: "SQLite", package: "SQLite.swift"),
.product(name: "SwiftSoup", package: "SwiftSoup"),
.product(name: "Tilt", package: "Tilt"),
.product(name: "Titlecaser", package: "Titlecaser"),
.product(name: "Yams", package: "Yams"),
]

#else

let coreDependencies: [Target.Dependency] = [
"PlatformSupport",
"Hoedown",
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdFoundation", package: "hummingbird"),
.product(name: "Licensable", package: "licensable"),
.product(name: "Logging", package: "swift-log"),
.product(name: "SQLite", package: "SQLite.swift"),
.product(name: "SwiftExif", package: "SwiftExif"),
.product(name: "SwiftSoup", package: "SwiftSoup"),
.product(name: "Tilt", package: "Tilt"),
.product(name: "Titlecaser", package: "Titlecaser"),
.product(name: "Yams", package: "Yams"),
]

#endif

let package = Package(
name: "incontext",
platforms: [
Expand Down Expand Up @@ -33,6 +71,7 @@ let package = Package(
.package(url: "https://github.com/jwells89/Titlecaser.git", from: "1.0.0"),
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"),
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.1"),
.package(url: "https://github.com/kradalby/SwiftExif.git", from: "0.0.7"),
],
targets: [
.executableTarget(
Expand All @@ -51,22 +90,7 @@ let package = Package(
]),
.target(
name: "InContextCore",
dependencies: [
"PlatformSupport",
"Hoedown",
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "FSEventsWrapper", package: "FSEventsWrapper", condition:
.when(platforms: [.macOS])),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdFoundation", package: "hummingbird"),
.product(name: "Licensable", package: "licensable"),
.product(name: "Logging", package: "swift-log"),
.product(name: "SQLite", package: "SQLite.swift"),
.product(name: "SwiftSoup", package: "SwiftSoup"),
.product(name: "Tilt", package: "Tilt"),
.product(name: "Titlecaser", package: "Titlecaser"),
.product(name: "Yams", package: "Yams"),
],
dependencies: coreDependencies,
resources: [
.process("Licenses"),
],
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ brew install inseven/incontext/incontext

See [https://incontext.app/docs](https://incontext.app/docs).

## Development

### Linux

Install third-party dependencies:

```bash
sudo dnf install libexif-devel libiptcdata-devel
````

## Frontmatter

Frontmatter is supported in Markdown files and image and video descriptions. InContext will pass through all unknown markdown fields, but puts type constraints on fields that have specific meaning:
Expand Down
37 changes: 34 additions & 3 deletions Sources/InContextCore/Extensions/Dictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,33 @@

import Foundation

protocol Transformable {

// TODO: I think this might actually be returning an Any?
// TODO: ASK TOM WTF
static func transform(from value: Any) -> Self?

}

extension Int: Transformable {

static func transform(from value: Any) -> Int? {
if let stringValue = value as? String {
return Int(stringValue)
}
return nil
}

}

extension String {

func convert<T>(type: T.Type) -> T? {
return nil
}

}

extension Dictionary {

func value<T>(for key: Key, default defaultValue: T) throws -> T {
Expand Down Expand Up @@ -51,10 +78,14 @@ extension Dictionary {
guard let value = self[key] else {
return nil
}
guard let value = value as? T else {
throw InContextError.incorrectType(expected: T.Type.self, received: value)
if let value = value as? T {
return value
}
return value
if let transformable = T.self as? Transformable.Type,
let value = transformable.transform(from: value) as? T {
return value
}
throw InContextError.incorrectType(expected: T.Type.self, received: value)
}

func requiredValue<T>(for key: Key) throws -> T {
Expand Down
83 changes: 31 additions & 52 deletions Sources/InContextCore/Importers/ImageImporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ import ImageIO

import PlatformSupport

#if !os(Linux)

protocol _Test {
// TODO: This needs to pass in the metadata
func evaluate(fileURL: URL) -> Bool
Expand Down Expand Up @@ -135,8 +133,8 @@ struct _Or<A: _Test, B: _Test>: _Test {
struct TransformContext {

let fileURL: URL
let imageSource: CGImageSource
let exif: EXIF
let image: PlatformImage
let exif: EXIF // TODO: This is encapsulated in `PlatformImage`

let assetsURL: URL

Expand Down Expand Up @@ -178,19 +176,23 @@ struct _Resize: _Transform {
let targetSize = size.fit(width: self.width)
let maxPixelSize = max(targetSize.width, targetSize.height)

let options = [kCGImageSourceCreateThumbnailWithTransform: kCFBooleanTrue,
kCGImageSourceCreateThumbnailFromImageAlways: kCFBooleanTrue,
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize as NSNumber] as CFDictionary

// TODO: Honour the input format if we don't have one.
guard let format = self.format ?? context.fileURL.type else {
throw InContextError.internalInconsistency("Failed to detect output type for '\(context.fileURL.relativePath)'.")
}

let frameCount = CGImageSourceGetCount(context.imageSource)

// TODO: Honour the input format if we don't have one.
let destinationFilename = basename + "." + (format.preferredFilenameExtension ?? "")
let destinationURL = context.assetsURL.appending(component: destinationFilename)

#if os(Linux)

#else

let options = [kCGImageSourceCreateThumbnailWithTransform: kCFBooleanTrue,
kCGImageSourceCreateThumbnailFromImageAlways: kCFBooleanTrue,
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize as NSNumber] as CFDictionary

let frameCount = CGImageSourceGetCount(context.image.source)

guard let destination = CGImageDestinationCreateWithURL(destinationURL as CFURL,
format.identifier as CFString,
frameCount,
Expand All @@ -200,7 +202,7 @@ struct _Resize: _Transform {

// Cherry-pick relevant image properties.
var destinationProperties: [String: Any] = [:]
if let sourceProperties = CGImageSourceCopyProperties(context.imageSource, nil) as? [String: Any] {
if let sourceProperties = CGImageSourceCopyProperties(context.image.source, nil) as? [String: Any] {
if let gifProperties = sourceProperties[kCGImagePropertyGIFDictionary as String] {
destinationProperties[kCGImagePropertyGIFDictionary as String] = gifProperties
}
Expand All @@ -209,8 +211,8 @@ struct _Resize: _Transform {

// Resize the frames.
for i in 0..<frameCount {
let thumbnail = CGImageSourceCreateThumbnailAtIndex(context.imageSource, i, options)!
if let properties = CGImageSourceCopyPropertiesAtIndex(context.imageSource, i, nil) as? [String: Any] {
let thumbnail = CGImageSourceCreateThumbnailAtIndex(context.image.source, i, options)!
if let properties = CGImageSourceCopyPropertiesAtIndex(context.image.source, i, nil) as? [String: Any] {
var frameProperties: [String: Any] = [:]
if let gifProperties = properties[kCGImagePropertyGIFDictionary as String] {
frameProperties[kCGImagePropertyGIFDictionary as String] = gifProperties
Expand All @@ -220,7 +222,10 @@ struct _Resize: _Transform {
}

CGImageDestinationFinalize(destination) // TODO: Handle error here?
context.assets.append(Asset(fileURL: destinationURL as URL))

#endif

// context.assets.append(Asset(fileURL: destinationURL as URL)) // TODO: Why `as URL`?

let details = [
"width": targetSize.width,
Expand Down Expand Up @@ -316,8 +321,6 @@ let configuration = _Configuration {

}

#endif

class ImageImporter {

struct Settings: ImporterSettings {
Expand Down Expand Up @@ -346,22 +349,6 @@ class ImageImporter {

}

#if os(Linux)

extension ImageImporter: Importer {

func process(file: File,
settings: Settings,
outputURL: URL) async throws -> ImporterResult {

throw InContextError.internalInconsistency("Unsupported")

}

}

#else

extension ImageImporter: Importer {

func process(file: File,
Expand All @@ -374,14 +361,8 @@ extension ImageImporter: Importer {
let assetsURL = URL(filePath: fileURL.relevantRelativePath, relativeTo: outputURL) // TODO: Make this a utiltiy and test it
try FileManager.default.createDirectory(at: assetsURL, withIntermediateDirectories: true)

// Load the original image.
guard let image = CGImageSourceCreateWithURL(fileURL as CFURL, nil) else {
throw InContextError.internalInconsistency("Failed to open image file at '\(fileURL.relativePath)'.")
}

guard let exif = try EXIF(image, 0) else {
throw InContextError.internalInconsistency("Failed to load properties for image at '\(fileURL.relativePath)'.")
}
// Load the image.
let image = try PlatformImage(url: fileURL)

let details = fileURL.basenameDetails()

Expand All @@ -392,13 +373,13 @@ extension ImageImporter: Importer {
metadata["scale"] = scale
}

if let projectionType = try exif.projectionType {
if let projectionType = try image.exif.projectionType {
metadata["projection"] = projectionType
}

// Content.
var content: FrontmatterDocument? = nil
if let imageDescription = try exif.imageDescription {
if let imageDescription = try image.exif.imageDescription {
let frontmatter = try FrontmatterDocument(contents: imageDescription, generateHTML: true)
guard let contentMetadata = frontmatter.metadata as? [String: Any] else {
throw InContextError.internalInconsistency("Unexpected key type for metadata")
Expand All @@ -408,8 +389,8 @@ extension ImageImporter: Importer {
}

// Location.
if let latitude = try exif.signedLatitude,
let longitude = try exif.signedLongitude {
if let latitude = try image.exif.signedLatitude,
let longitude = try image.exif.signedLongitude {
metadata["location"] = [
"latitude": latitude,
"longitude": longitude,
Expand All @@ -418,8 +399,8 @@ extension ImageImporter: Importer {

// Perform the transforms.
var context = TransformContext(fileURL: fileURL,
imageSource: image,
exif: exif,
image: image,
exif: image.exif,
assetsURL: assetsURL,
metadata: metadata,
assets: [])
Expand All @@ -438,8 +419,8 @@ extension ImageImporter: Importer {

// N.B. The EXIF 'DateTimeOriginal' field sometimes appears to be invalid so we fall back on DateTimeDigitized.

let date = try (try? exif.dateTimeOriginal) ?? (try exif.dateTimeDigitized) ?? details.date
let title = try exif.firstTitle ?? content?.title ?? filenameTitle
let date = try (try? image.exif.dateTimeOriginal) ?? (try image.exif.dateTimeDigitized) ?? details.date
let title = try image.exif.firstTitle ?? content?.title ?? filenameTitle

let document = try Document(url: fileURL.siteURL,
parent: fileURL.parentURL,
Expand All @@ -457,5 +438,3 @@ extension ImageImporter: Importer {
}

}

#endif
Loading
Loading