mirror of
https://github.com/bitsycore/Allofit.git
synced 2026-10-05 12:27:26 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e67da4634 | ||
|
|
bac2ca0287 |
@@ -55,10 +55,19 @@ enum AllofitService {
|
|||||||
// the autosave thread (every 3s) can write partial progress while we
|
// the autosave thread (every 3s) can write partial progress while we
|
||||||
// continue walking. Without this, large filesystems leave the cache
|
// continue walking. Without this, large filesystems leave the cache
|
||||||
// empty for many minutes and the GUI shows nothing.
|
// empty for many minutes and the GUI shows nothing.
|
||||||
|
//
|
||||||
|
// Both the per-root and per-batch callback bodies run inside their
|
||||||
|
// own autoreleasepool so the autoreleased NSURL / NSDate / NSNumber
|
||||||
|
// from the file enumeration don't pile up until the entire scan
|
||||||
|
// finishes - on a million-file scan that "pile" was peaking at
|
||||||
|
// well over a gig of dead allocations before the main thread's
|
||||||
|
// runloop got a chance to drain.
|
||||||
let vStartId = UInt64(FSEventsGetCurrentEventId())
|
let vStartId = UInt64(FSEventsGetCurrentEventId())
|
||||||
for vRoot in vRoots {
|
for vRoot in vRoots {
|
||||||
|
autoreleasepool {
|
||||||
NSLog("[Allofit] scanning %@", vRoot.path)
|
NSLog("[Allofit] scanning %@", vRoot.path)
|
||||||
FileIndexer.walkRoot(inRoot: vRoot, inExclusions: vMatcher) { vBatch in
|
FileIndexer.walkRoot(inRoot: vRoot, inExclusions: vMatcher) { vBatch in
|
||||||
|
autoreleasepool {
|
||||||
vState.lock.lock()
|
vState.lock.lock()
|
||||||
for vRec in vBatch {
|
for vRec in vBatch {
|
||||||
if vMatcher.isExcluded(inPath: vRec.fullPath) { continue }
|
if vMatcher.isExcluded(inPath: vRec.fullPath) { continue }
|
||||||
@@ -70,8 +79,10 @@ enum AllofitService {
|
|||||||
vState.dirty = true
|
vState.dirty = true
|
||||||
vState.lock.unlock()
|
vState.lock.unlock()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
NSLog("[Allofit] scanned %@: %d total entries so far", vRoot.path, vState.records.count)
|
NSLog("[Allofit] scanned %@: %d total entries so far", vRoot.path, vState.records.count)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// force one save right after the scan finishes, so the GUI sees a
|
// force one save right after the scan finishes, so the GUI sees a
|
||||||
// stable count even if no FSEvents come in for a while afterwards
|
// stable count even if no FSEvents come in for a while afterwards
|
||||||
IndexStore.save(inRecords: vState.records, inLastEventId: vStartId)
|
IndexStore.save(inRecords: vState.records, inLastEventId: vStartId)
|
||||||
@@ -98,14 +109,21 @@ enum AllofitService {
|
|||||||
// dominating CPU and turning into the daemon's memory churn
|
// dominating CPU and turning into the daemon's memory churn
|
||||||
var vRemoved: Set<String> = []
|
var vRemoved: Set<String> = []
|
||||||
|
|
||||||
|
// Per-change autoreleasepool: a large FSEvents batch (e.g.
|
||||||
|
// `rm -rf` of a deep tree) can deliver thousands of paths in
|
||||||
|
// one callback. Each path's NSURL + reachability check + the
|
||||||
|
// makeRecord internals autorelease - per-change drain keeps
|
||||||
|
// peak memory bounded by a single record's worth, not the
|
||||||
|
// whole batch.
|
||||||
for vChange in vChanges {
|
for vChange in vChanges {
|
||||||
if vMatcher.isExcluded(inPath: vChange.path) { continue }
|
autoreleasepool {
|
||||||
|
if vMatcher.isExcluded(inPath: vChange.path) { return }
|
||||||
if vChange.mustScanSubDirs {
|
if vChange.mustScanSubDirs {
|
||||||
vRescanPrefixes.append(vChange.path)
|
vRescanPrefixes.append(vChange.path)
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
// skip paths we've already queued for removal in this batch
|
// skip paths we've already queued for removal in this batch
|
||||||
if vRemoved.contains(vChange.path) { continue }
|
if vRemoved.contains(vChange.path) { return }
|
||||||
let vUrl = URL(fileURLWithPath: vChange.path)
|
let vUrl = URL(fileURLWithPath: vChange.path)
|
||||||
let vExists = (try? vUrl.checkResourceIsReachable()) ?? false
|
let vExists = (try? vUrl.checkResourceIsReachable()) ?? false
|
||||||
if vExists, let vRec = FileIndexer.makeRecord(inURL: vUrl) {
|
if vExists, let vRec = FileIndexer.makeRecord(inURL: vUrl) {
|
||||||
@@ -121,6 +139,7 @@ enum AllofitService {
|
|||||||
vRemoved.insert(vChange.path)
|
vRemoved.insert(vChange.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !vRemoved.isEmpty {
|
if !vRemoved.isEmpty {
|
||||||
// single bulk removeAll + single pathIndex rebuild
|
// single bulk removeAll + single pathIndex rebuild
|
||||||
@@ -162,17 +181,25 @@ enum AllofitService {
|
|||||||
// Array/Dict capacity (which only grows on churn, never auto-shrinks)
|
// Array/Dict capacity (which only grows on churn, never auto-shrinks)
|
||||||
// doesn't drift into multi-GB territory after a day of heavy file
|
// doesn't drift into multi-GB territory after a day of heavy file
|
||||||
// activity. RSS is logged each save so the trajectory is visible.
|
// activity. RSS is logged each save so the trajectory is visible.
|
||||||
|
//
|
||||||
|
// CRITICAL: each iteration runs inside its own autoreleasepool. The
|
||||||
|
// outer GCD block has an autorelease pool that drains when the block
|
||||||
|
// returns - which for our `while true` never happens. Without an
|
||||||
|
// inner pool, every save's autoreleased NSData (returned by
|
||||||
|
// .compressed(using: .lz4) and friends) accumulates forever and the
|
||||||
|
// daemon's RSS grows by tens of MB per save (500MB+/min in practice).
|
||||||
DispatchQueue.global(qos: .utility).async {
|
DispatchQueue.global(qos: .utility).async {
|
||||||
let kCompactEvery = 20
|
let kCompactEvery = 20
|
||||||
var vSavesSinceCompact = 0
|
var vSavesSinceCompact = 0
|
||||||
while true {
|
while true {
|
||||||
sleep(3)
|
sleep(3)
|
||||||
|
autoreleasepool {
|
||||||
vState.lock.lock()
|
vState.lock.lock()
|
||||||
let vShouldSave = vState.dirty
|
let vShouldSave = vState.dirty
|
||||||
let vSnapshot = vState.records
|
let vSnapshot = vState.records
|
||||||
vState.dirty = false
|
vState.dirty = false
|
||||||
vState.lock.unlock()
|
vState.lock.unlock()
|
||||||
if !vShouldSave { continue }
|
if !vShouldSave { return }
|
||||||
NSLog("[Allofit] autosaving %d records (RSS %.1f MB)",
|
NSLog("[Allofit] autosaving %d records (RSS %.1f MB)",
|
||||||
vSnapshot.count, processFootprintMB())
|
vSnapshot.count, processFootprintMB())
|
||||||
IndexStore.save(
|
IndexStore.save(
|
||||||
@@ -186,6 +213,7 @@ enum AllofitService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// block forever on the runloop so launchd keeps us alive
|
// block forever on the runloop so launchd keeps us alive
|
||||||
RunLoop.current.run()
|
RunLoop.current.run()
|
||||||
|
|||||||
@@ -241,6 +241,10 @@ final class AppModel: ObservableObject {
|
|||||||
var vAccumulated: [FileRecord] = []
|
var vAccumulated: [FileRecord] = []
|
||||||
vAccumulated.reserveCapacity(200_000)
|
vAccumulated.reserveCapacity(200_000)
|
||||||
for vRoot in vRoots {
|
for vRoot in vRoots {
|
||||||
|
// per-root autoreleasepool so the file-enumeration's
|
||||||
|
// autoreleased NSURL/NSDate/NSNumber objects don't pile
|
||||||
|
// up across roots inside this long-running async block
|
||||||
|
autoreleasepool {
|
||||||
let vBaseline = vAccumulated.count
|
let vBaseline = vAccumulated.count
|
||||||
let vList = FileIndexer.indexRoot(inRoot: vRoot, inExclusions: vMatcher) { vCount in
|
let vList = FileIndexer.indexRoot(inRoot: vRoot, inExclusions: vMatcher) { vCount in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
@@ -249,6 +253,7 @@ final class AppModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
vAccumulated.append(contentsOf: vList)
|
vAccumulated.append(contentsOf: vList)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let vFinal = vAccumulated
|
let vFinal = vAccumulated
|
||||||
let vLookup = AppModel.buildPathLookup(inRecords: vFinal)
|
let vLookup = AppModel.buildPathLookup(inRecords: vFinal)
|
||||||
// persist off-main before bouncing back so main never sees the
|
// persist off-main before bouncing back so main never sees the
|
||||||
@@ -494,12 +499,17 @@ final class AppModel: ObservableObject {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for vPath in inPaths {
|
for vPath in inPaths {
|
||||||
|
// per-subtree autoreleasepool keeps the rescan's
|
||||||
|
// autoreleased URL/stat objects from accumulating across
|
||||||
|
// subtrees inside this long-running async block
|
||||||
|
autoreleasepool {
|
||||||
let vList = FileIndexer.indexRoot(
|
let vList = FileIndexer.indexRoot(
|
||||||
inRoot: URL(fileURLWithPath: vPath),
|
inRoot: URL(fileURLWithPath: vPath),
|
||||||
inExclusions: inExclusions
|
inExclusions: inExclusions
|
||||||
)
|
)
|
||||||
vKept.append(contentsOf: vList)
|
vKept.append(contentsOf: vList)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let vFinal = vKept
|
let vFinal = vKept
|
||||||
let vLookup = AppModel.buildPathLookup(inRecords: vFinal)
|
let vLookup = AppModel.buildPathLookup(inRecords: vFinal)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|||||||
@@ -49,14 +49,21 @@ enum FileIndexer {
|
|||||||
vBatch.append(vRootRecord)
|
vBatch.append(vRootRecord)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-iteration autoreleasepool: every URL pulled from the
|
||||||
|
// enumerator + every resourceValues() read autoreleases an
|
||||||
|
// NSURL / NSDate / NSNumber. Without this drain a million-file
|
||||||
|
// walk would let those accumulate into hundreds of MB of dead
|
||||||
|
// allocations until the enclosing async block exited - and the
|
||||||
|
// daemon's enclosing block never exits.
|
||||||
for vCase in vEnumerator {
|
for vCase in vEnumerator {
|
||||||
guard let vURL = vCase as? URL else { continue }
|
autoreleasepool {
|
||||||
|
guard let vURL = vCase as? URL else { return }
|
||||||
|
|
||||||
// short-circuit excluded entries (and don't descend into them)
|
// short-circuit excluded entries (and don't descend into them)
|
||||||
if let vMatcher = inExclusions, vMatcher.isExcluded(inPath: vURL.path) {
|
if let vMatcher = inExclusions, vMatcher.isExcluded(inPath: vURL.path) {
|
||||||
let vIsDir = (try? vURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false
|
let vIsDir = (try? vURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false
|
||||||
if vIsDir { vEnumerator.skipDescendants() }
|
if vIsDir { vEnumerator.skipDescendants() }
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let vRecord = makeRecord(inURL: vURL) {
|
if let vRecord = makeRecord(inURL: vURL) {
|
||||||
@@ -67,6 +74,7 @@ enum FileIndexer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if !vBatch.isEmpty {
|
if !vBatch.isEmpty {
|
||||||
inBatch(vBatch)
|
inBatch(vBatch)
|
||||||
}
|
}
|
||||||
@@ -99,8 +107,17 @@ enum FileIndexer {
|
|||||||
// builds a FileRecord from a URL's pre-fetched resource values.
|
// builds a FileRecord from a URL's pre-fetched resource values.
|
||||||
// Clears the URL's resource-value cache first so we always re-stat the
|
// Clears the URL's resource-value cache first so we always re-stat the
|
||||||
// file - a file modified between two FSEvents batches would otherwise
|
// file - a file modified between two FSEvents batches would otherwise
|
||||||
// silently return the cached pre-edit mtime.
|
// silently return the cached pre-edit mtime. The whole body is wrapped
|
||||||
|
// in autoreleasepool so the autoreleased NSDate / NSNumber / NSURL
|
||||||
|
// objects returned by resourceValues() are released at function exit
|
||||||
|
// rather than at the caller's pool drain.
|
||||||
static func makeRecord(inURL: URL) -> FileRecord? {
|
static func makeRecord(inURL: URL) -> FileRecord? {
|
||||||
|
return autoreleasepool { () -> FileRecord? in
|
||||||
|
makeRecord_impl(inURL: inURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeRecord_impl(inURL: URL) -> FileRecord? {
|
||||||
var vUrl = inURL
|
var vUrl = inURL
|
||||||
vUrl.removeAllCachedResourceValues()
|
vUrl.removeAllCachedResourceValues()
|
||||||
guard let vValues = try? vUrl.resourceValues(forKeys: Set(kPrefetchKeys)) else {
|
guard let vValues = try? vUrl.resourceValues(forKeys: Set(kPrefetchKeys)) else {
|
||||||
|
|||||||
@@ -91,8 +91,18 @@ enum IndexStore {
|
|||||||
save(inRecords: inRecords, inLastEventId: inLastEventId, to: cacheURL)
|
save(inRecords: inRecords, inLastEventId: inLastEventId, to: cacheURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writes the records and event id atomically to a specific URL
|
// writes the records and event id atomically to a specific URL.
|
||||||
|
// Wrapped in autoreleasepool because every call autoreleases a number
|
||||||
|
// of Foundation objects (the compressed NSData, NSURLs created by
|
||||||
|
// FileManager.replaceItem, etc.) - if the caller is a long-running
|
||||||
|
// block whose own pool never drains, those would accumulate forever.
|
||||||
static func save(inRecords: [FileRecord], inLastEventId: UInt64, to inUrl: URL) {
|
static func save(inRecords: [FileRecord], inLastEventId: UInt64, to inUrl: URL) {
|
||||||
|
autoreleasepool {
|
||||||
|
save_impl(inRecords: inRecords, inLastEventId: inLastEventId, to: inUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func save_impl(inRecords: [FileRecord], inLastEventId: UInt64, to inUrl: URL) {
|
||||||
var vPayload = Data()
|
var vPayload = Data()
|
||||||
vPayload.reserveCapacity(16 + inRecords.count * 80)
|
vPayload.reserveCapacity(16 + inRecords.count * 80)
|
||||||
writeU64(into: &vPayload, value: inLastEventId)
|
writeU64(into: &vPayload, value: inLastEventId)
|
||||||
@@ -151,8 +161,16 @@ enum IndexStore {
|
|||||||
return load(from: cacheURL)
|
return load(from: cacheURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// reads the persisted index back from a specific URL
|
// reads the persisted index back from a specific URL. Wrapped in
|
||||||
|
// autoreleasepool so the decompressed NSData and the per-record
|
||||||
|
// String allocations don't linger in the caller's pool.
|
||||||
static func load(from inUrl: URL) -> LoadResult? {
|
static func load(from inUrl: URL) -> LoadResult? {
|
||||||
|
return autoreleasepool {
|
||||||
|
load_impl(from: inUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func load_impl(from inUrl: URL) -> LoadResult? {
|
||||||
guard let vData = try? Data(contentsOf: inUrl) else { return nil }
|
guard let vData = try? Data(contentsOf: inUrl) else { return nil }
|
||||||
var vOffset = 0
|
var vOffset = 0
|
||||||
guard let vMagic = readU32(from: vData, offset: &vOffset), vMagic == kMagic else { return nil }
|
guard let vMagic = readU32(from: vData, offset: &vOffset), vMagic == kMagic else { return nil }
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
#!/usr/bin/env swift
|
|
||||||
//
|
|
||||||
// Generates a 1024x1024 placeholder app icon at icons/icon.png so build-app.sh
|
|
||||||
// has something to bake into Allofit.app. Designed to be "Icon Composer ready"
|
|
||||||
// - the output is a single flat 1024x1024 PNG, which is exactly what Apple's
|
|
||||||
// Icon Composer (macOS 26+) takes as a base layer when you want to refine
|
|
||||||
// the icon with Liquid Glass effects.
|
|
||||||
//
|
|
||||||
// Run from the project root:
|
|
||||||
// swift make-placeholder-icon.swift
|
|
||||||
// ./build-app.sh # picks up icons/icon.png automatically
|
|
||||||
//
|
|
||||||
// Tweak the colors / shape constants below and re-run to iterate.
|
|
||||||
|
|
||||||
import AppKit
|
|
||||||
import CoreGraphics
|
|
||||||
import ImageIO
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: Tunables
|
|
||||||
// ==================
|
|
||||||
|
|
||||||
// Final image side length (px). 1024 is the largest slot the macOS iconset
|
|
||||||
// asks for so it works for every output size after sips downscales.
|
|
||||||
let kSizePixels: Int = 1024
|
|
||||||
// Apple's rounded-square corner radius is ~22.37% of side (classic) or
|
|
||||||
// ~25% (macOS 26 "Tahoe"). 22.37% is a safe middle ground.
|
|
||||||
let kCornerFactor: CGFloat = 0.2237
|
|
||||||
// Gradient stops (top-left to bottom-right): blue → teal
|
|
||||||
let kGradientStart = CGColor(srgbRed: 0.00, green: 0.48, blue: 1.00, alpha: 1)
|
|
||||||
let kGradientEnd = CGColor(srgbRed: 0.13, green: 0.71, blue: 0.92, alpha: 1)
|
|
||||||
// Magnifying glass styling (in normalised 0..1 of the canvas)
|
|
||||||
let kGlassCenterX: CGFloat = 0.42
|
|
||||||
let kGlassCenterY: CGFloat = 0.58
|
|
||||||
let kGlassRadius: CGFloat = 0.20
|
|
||||||
let kHandleLength: CGFloat = 0.22
|
|
||||||
let kStrokeWidth: CGFloat = 0.06
|
|
||||||
// Output relative path
|
|
||||||
let kOutputPath = "icons/icon.png"
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: Draw
|
|
||||||
// ==================
|
|
||||||
|
|
||||||
let vSize = CGFloat(kSizePixels)
|
|
||||||
guard let vColorSpace = CGColorSpace(name: CGColorSpace.sRGB) else {
|
|
||||||
fputs("could not create sRGB color space\n", stderr); exit(1)
|
|
||||||
}
|
|
||||||
guard let ctx = CGContext(
|
|
||||||
data: nil,
|
|
||||||
width: kSizePixels,
|
|
||||||
height: kSizePixels,
|
|
||||||
bitsPerComponent: 8,
|
|
||||||
bytesPerRow: 0,
|
|
||||||
space: vColorSpace,
|
|
||||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
|
||||||
) else {
|
|
||||||
fputs("could not create CG context\n", stderr); exit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rounded-square mask matching Apple's app icon silhouette
|
|
||||||
let vRect = CGRect(x: 0, y: 0, width: vSize, height: vSize)
|
|
||||||
let vCornerRadius = vSize * kCornerFactor
|
|
||||||
let vMaskPath = CGPath(roundedRect: vRect,
|
|
||||||
cornerWidth: vCornerRadius,
|
|
||||||
cornerHeight: vCornerRadius,
|
|
||||||
transform: nil)
|
|
||||||
|
|
||||||
// Gradient background, clipped to the rounded square
|
|
||||||
ctx.saveGState()
|
|
||||||
ctx.addPath(vMaskPath)
|
|
||||||
ctx.clip()
|
|
||||||
let vGradient = CGGradient(
|
|
||||||
colorsSpace: vColorSpace,
|
|
||||||
colors: [kGradientStart, kGradientEnd] as CFArray,
|
|
||||||
locations: [0.0, 1.0]
|
|
||||||
)!
|
|
||||||
ctx.drawLinearGradient(vGradient,
|
|
||||||
start: CGPoint(x: 0, y: vSize),
|
|
||||||
end: CGPoint(x: vSize, y: 0),
|
|
||||||
options: [])
|
|
||||||
ctx.restoreGState()
|
|
||||||
|
|
||||||
// White magnifying glass on top
|
|
||||||
ctx.setStrokeColor(CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1))
|
|
||||||
ctx.setLineWidth(vSize * kStrokeWidth)
|
|
||||||
ctx.setLineCap(.round)
|
|
||||||
|
|
||||||
let vGlassCenter = CGPoint(x: vSize * kGlassCenterX, y: vSize * kGlassCenterY)
|
|
||||||
let vGlassR = vSize * kGlassRadius
|
|
||||||
ctx.strokeEllipse(in: CGRect(
|
|
||||||
x: vGlassCenter.x - vGlassR,
|
|
||||||
y: vGlassCenter.y - vGlassR,
|
|
||||||
width: vGlassR * 2,
|
|
||||||
height: vGlassR * 2
|
|
||||||
))
|
|
||||||
|
|
||||||
// Handle: from the lower-right edge of the glass, going down-right at -45°
|
|
||||||
let vAngle: CGFloat = -.pi / 4
|
|
||||||
let vHandleStart = CGPoint(
|
|
||||||
x: vGlassCenter.x + vGlassR * cos(vAngle),
|
|
||||||
y: vGlassCenter.y + vGlassR * sin(vAngle)
|
|
||||||
)
|
|
||||||
let vHandleLen = vSize * kHandleLength
|
|
||||||
let vHandleEnd = CGPoint(
|
|
||||||
x: vHandleStart.x + vHandleLen * cos(vAngle),
|
|
||||||
y: vHandleStart.y + vHandleLen * sin(vAngle)
|
|
||||||
)
|
|
||||||
ctx.move(to: vHandleStart)
|
|
||||||
ctx.addLine(to: vHandleEnd)
|
|
||||||
ctx.strokePath()
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: Write PNG
|
|
||||||
// ==================
|
|
||||||
|
|
||||||
guard let vCgImage = ctx.makeImage() else {
|
|
||||||
fputs("could not create CGImage from context\n", stderr); exit(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
try? FileManager.default.createDirectory(
|
|
||||||
atPath: (kOutputPath as NSString).deletingLastPathComponent,
|
|
||||||
withIntermediateDirectories: true
|
|
||||||
)
|
|
||||||
let vOutputURL = URL(fileURLWithPath: kOutputPath)
|
|
||||||
guard let vDest = CGImageDestinationCreateWithURL(
|
|
||||||
vOutputURL as CFURL,
|
|
||||||
"public.png" as CFString,
|
|
||||||
1,
|
|
||||||
nil
|
|
||||||
) else {
|
|
||||||
fputs("could not create image destination\n", stderr); exit(4)
|
|
||||||
}
|
|
||||||
CGImageDestinationAddImage(vDest, vCgImage, nil)
|
|
||||||
guard CGImageDestinationFinalize(vDest) else {
|
|
||||||
fputs("PNG finalize failed\n", stderr); exit(5)
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Wrote \(kOutputPath) (\(kSizePixels)x\(kSizePixels))")
|
|
||||||
Reference in New Issue
Block a user