Skip to content
Merged
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
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
build-and-test:
runs-on: macos-15
steps:
- uses: actions/checkout@v4

- name: Build
run: swift build

- name: Run core tests with coverage
run: ./script/test.sh
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The project is early, useful, and intentionally open-ended: the renderer is prof
- Multi-photo import, `.filmchef` project files, missing-photo relink, edit history, undo/redo, and named variants
- Generated app bundles declare Film Chef project document ownership for `.filmchef` files
- Non-destructive local adjustment layers with radial, linear, brush, and path masks, including direct preview editing
- Original, edited, split, and side-by-side preview modes with bounded pan, loupe placement, zoom, and draggable comparison
- Original, edited, and split preview modes with bounded pan, loupe placement, zoom, and draggable comparison
- RGB, luminance, and RGB parade scopes with clipping readouts and pixel sampling
- ImageIO export to JPEG, PNG, or TIFF with quality, scale, metadata, ICC profile, and validated naming templates
- Export preset management with duplicate, update, delete, restore defaults, and project persistence
Expand Down
7 changes: 0 additions & 7 deletions Sources/FilmChefCore/Models/EditorModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,6 @@ package enum PreviewComparisonMode: String, CaseIterable, Codable, Equatable, Id
case edited
case original
case split
case sideBySide

package static var allCases: [PreviewComparisonMode] {
[.edited, .original, .split]
}

package var id: String { rawValue }

Expand All @@ -296,8 +291,6 @@ package enum PreviewComparisonMode: String, CaseIterable, Codable, Equatable, Id
return "Original"
case .split:
return "Split"
case .sideBySide:
return "Side by Side"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/FilmChefCore/Services/FilmPipelineRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ package struct FilmPipelineRenderer {
)

var noise = random
.transformed(by: CGAffineTransform(scaleX: 1.0 / size, y: 1.0 / size))
.transformed(by: CGAffineTransform(scaleX: size, y: size))
.cropped(to: extent)

let softnessRadius = grain.softness * size * 1.6
Expand Down
23 changes: 20 additions & 3 deletions Sources/FilmChefCore/Services/ImageProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ package final class ImageProcessor {
let rendered = context.createCGImage(
applyRawDevelopment(to: image, settings: colorSettings),
from: image.extent,
format: .RGBA8,
format: .RGBAh,
colorSpace: workingColorSpace(for: colorSettings.workingColorSpace)
)
else {
Expand All @@ -63,6 +63,15 @@ package final class ImageProcessor {
return CIImage(cgImage: rendered)
}

package func validateReadableImage(at url: URL) throws {
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
CGImageSourceGetCount(source) > 0,
CGImageSourceCreateImageAtIndex(source, 0, nil) != nil
else {
throw ImageProcessorError.cannotLoadImage
}
}

package func makePreviewImage(
from source: CIImage,
maxDimension: CGFloat = ImageProcessor.defaultPreviewMaxDimension
Expand Down Expand Up @@ -243,10 +252,10 @@ package final class ImageProcessor {
greenParade[paradeBin] += g / 255.0
blueParade[paradeBin] += b / 255.0

if r <= 1 || g <= 1 || b <= 1 {
if r <= 1, g <= 1, b <= 1 {
shadowClipped += 1
}
if r >= 254 || g >= 254 || b >= 254 {
if r >= 254, g >= 254, b >= 254 {
highlightClipped += 1
}
}
Expand Down Expand Up @@ -650,6 +659,10 @@ package final class ImageProcessor {
return CGColorSpace(name: CGColorSpace.displayP3) ?? CGColorSpaceCreateDeviceRGB()
}

if normalized.contains("extended"), normalized.contains("linear") {
return CGColorSpace(name: CGColorSpace.extendedLinearSRGB) ?? CGColorSpaceCreateDeviceRGB()
}

if normalized.contains("extended") {
return CGColorSpace(name: CGColorSpace.extendedSRGB) ?? CGColorSpaceCreateDeviceRGB()
}
Expand All @@ -671,6 +684,10 @@ package final class ImageProcessor {
return "Display P3"
}

if normalized.contains("extended"), normalized.contains("linear") {
return "Extended Linear sRGB"
}

if normalized.contains("extended") {
return "Extended sRGB"
}
Expand Down
24 changes: 20 additions & 4 deletions Sources/FilmChefCore/Stores/EditorStore+ExportNaming.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,20 @@ extension EditorStore {
return Array(Set(issues)).sorted()
}

nonisolated package static func uniqueExportURL(
in directory: URL,
nonisolated package static func exportFileName(
item: FilmProjectItem,
recipe: FilmRecipe,
settings: ExportSettings
) -> URL {
let baseName = sanitizedFileComponent(
) -> String {
"\(exportBaseName(item: item, recipe: recipe, settings: settings)).\(settings.fileFormat.preferredPathExtension)"
}

nonisolated private static func exportBaseName(
item: FilmProjectItem,
recipe: FilmRecipe,
settings: ExportSettings
) -> String {
sanitizedFileComponent(
settings.namingTemplate
.replacingOccurrences(
of: "{photo}",
Expand All @@ -55,6 +62,15 @@ extension EditorStore {
.replacingOccurrences(of: "{recipe}", with: recipe.name)
.replacingOccurrences(of: "{format}", with: settings.fileFormat.label)
)
}

nonisolated package static func uniqueExportURL(
in directory: URL,
item: FilmProjectItem,
recipe: FilmRecipe,
settings: ExportSettings
) -> URL {
let baseName = exportBaseName(item: item, recipe: recipe, settings: settings)
let fileExtension = settings.fileFormat.preferredPathExtension
var candidate = directory.appendingPathComponent("\(baseName).\(fileExtension)")
var suffix = 2
Expand Down
Loading
Loading