Use atproto actions with ease in iOS shortcuts
1//
2// FileIntentToImageQuery.swift
3// shortcut
4//
5// Created by Bailey Townsend on 6/30/25.
6//
7
8import ATProtoKit
9import AppIntents
10import CoreGraphics
11import Foundation
12import ImageIO
13import Photos
14import UIKit
15
16public func IntentFileToImageQuery(files: [IntentFile], altText: [String]) async throws
17 -> [ATProtoTools.ImageQuery]
18{
19 var imageQueries: [ATProtoTools.ImageQuery] = []
20
21 for (index, image) in files.enumerated() {
22
23 if image.availableContentTypes.contains(.heic) {
24 let heicImage = try await image.withFile(
25 contentType: .heic,
26 fileHandler: { url, openInPlace in
27 guard
28 let image = UIImage(contentsOfFile: url.absoluteURL.path())
29 else {
30 throw GenericIntentError.message(
31 "Failed to load image \(image.filename).")
32 }
33 return image
34 }
35 )
36 let fileName = image.filename
37 if let jpegData = heicImage.jpegData(compressionQuality: 0.1) {
38 imageQueries.append(
39
40 ATProtoTools.ImageQuery(
41 imageData: jpegData, fileName: fileName,
42 altText: altText[safe: index],
43 aspectRatio: AppBskyLexicon.Embed.AspectRatioDefinition(
44 width: Int(heicImage.size.width),
45 height: Int(heicImage.size.height))
46 ))
47 }
48
49 } else if image.availableContentTypes.contains(.jpeg) {
50 let jpeg = try await image.withFile(
51 contentType: .jpeg,
52 fileHandler: { url, openInPlace in
53 guard
54 let image = UIImage(contentsOfFile: url.absoluteURL.path())
55 else {
56 throw GenericIntentError.message(
57 "Failed to load image \(image.filename).")
58 }
59 return image
60 }
61 )
62 if let jpegData = jpeg.jpegData(compressionQuality: 0.1) {
63 imageQueries.append(
64 ATProtoTools.ImageQuery(
65 imageData: jpegData, fileName: image.filename,
66 altText: altText[safe: index],
67 aspectRatio: AppBskyLexicon.Embed.AspectRatioDefinition(
68 width: Int(jpeg.size.width), height: Int(jpeg.size.height))
69 ))
70 }
71
72 } else if image.availableContentTypes.contains(.png) {
73 let png = try await image.withFile(
74 contentType: .png,
75 fileHandler: { url, openInPlace in
76 guard
77 let image = UIImage(contentsOfFile: url.absoluteURL.path())
78 else {
79 throw GenericIntentError.message(
80 "Failed to load image \(image.filename).")
81 }
82 return image
83 }
84 )
85
86 if let jpegData = png.jpegData(compressionQuality: 0.1) {
87 imageQueries.append(
88 ATProtoTools.ImageQuery(
89 imageData: jpegData, fileName: image.filename,
90 altText: altText[safe: index],
91 aspectRatio: AppBskyLexicon.Embed.AspectRatioDefinition(
92 width: Int(png.size.width), height: Int(png.size.height))
93 ))
94 }
95
96 } else {
97 //Just yolo it
98 imageQueries.append(
99 ATProtoTools.ImageQuery(
100 imageData: image.data, fileName: image.filename,
101 altText: altText[safe: index], aspectRatio: nil
102 ))
103 }
104 }
105 return imageQueries
106}
107
108private func extractCaptionFromImageData(_ data: Data) -> String? {
109 guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else {
110 return nil
111 }
112
113 guard let metadata = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any]
114 else {
115 return nil
116 }
117
118 // Check EXIF UserComment
119 // if let exifDict = metadata[kCGImagePropertyExifDictionary as String] as? [String: Any],
120 // let userComment = exifDict[kCGImagePropertyExifUserComment as String] as? String
121 // {
122 // return userComment.isEmpty ? nil : userComment
123 // }
124
125 // Check IPTC Caption
126 // if let iptcDict = metadata[kCGImagePropertyIPTCDictionary as String] as? [String: Any],
127 // let caption = iptcDict[kCGImagePropertyIPTCCaptionAbstract as String] as? String
128 // {
129 // return caption.isEmpty ? nil : caption
130 // }
131
132 // Check TIFF ImageDescription
133 if let tiffDict = metadata[kCGImagePropertyTIFFDictionary as String] as? [String: Any],
134 let description = tiffDict[kCGImagePropertyTIFFImageDescription as String] as? String
135 {
136 return description.isEmpty ? nil : description
137 }
138
139 return nil
140}
141
142extension Array {
143 subscript(safe index: Int) -> Element? {
144 return indices.contains(index) ? self[index] : nil
145 }
146}