Open19

NSAdaptiveImageGlyphをハックしてカスタム絵文字を通知欄に出したい

noppenoppe

文中からNSAdaptiveImageGlyphを取り出す

let range = NSRange(location: 0, length: textView.attributedText.length)
textView.attributedText.enumerateAttribute(.adaptiveImageGlyph, in: range) { adaptiveImageProvider, range, _ in
 let imageGlyph = (adaptiveImageProvider as! NSAdaptiveImageGlyph)
 print(imageGlyph.contentIdentifier)
 print(imageGlyph.contentDescription)
 print(imageGlyph.imageContent)
}
noppenoppe

imageContentはheicのメタデータ拡張であると考えられる

noppenoppe
let url = Bundle.main.url(forResource: "NSAdaptiveImageGlyphImageContent", withExtension: "heic")!
let data = try! Data(contentsOf: url)
let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil)!
let metadata = CGImageSourceCopyMetadataAtIndex(imageSource, 0, nil)!
print(metadata)        
<CGImageMetadata 0x158092440> (
    tiff:DocumentName = CE0AF467-2EB5-481E-9B53-DA745BC26BF20
    tiff:XPosition = 0/1
    tiff:TileWidth = 160
    tiff:YPosition = 0/1
    dc:description = (
            "<CGImageMetadataTag 0x302a08640> dc:[x-default] = fox glasses, Qualifiers = (\n    \"<CGImageMetadataTag 0x302a086e0> xml:lang = x-default\"\n)"
        )
    Iptc4xmpExt:DigitalSourceType = http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia
    tiff:TileLength = 160
    photoshop:Credit = Apple Image Playground
    tiff:Orientation = 1
    iio:hasXMP = True
    xmp:CreatorTool = Apple TextKit
)
noppenoppe

heicの中に複数のサイズの画像が格納されている

  • 160
  • 40
  • 64
  • 96
  • 320
noppenoppe

tiff:DocumentName = CE0AF467-2EB5-481E-9B53-DA745BC26BF20

これはNSAdaptiveImageGlyphのcontentIdentifierと一致する

noppenoppe
let url = Bundle.main.url(forResource: "NSAdaptiveImageGlyphImageContent", withExtension: "heic")!
let data = try! Data(contentsOf: url)
let adaptiveImageGlyph = NSAdaptiveImageGlyph(imageContent: data)
print(adaptiveImageGlyph.contentIdentifier)

このheicはNSAdaptiveImageGlyphのimageContentとして復元可能

noppenoppe

復元したNSAdaptiveImageGlyphは挿入可能

let url = Bundle.main.url(forResource: "NSAdaptiveImageGlyphImageContent", withExtension: "heic")!
let data = try! Data(contentsOf: url)
let adaptiveImageGlyph = NSAdaptiveImageGlyph(imageContent: data)
let attr = NSAttributedString(adaptiveImageGlyph: adaptiveImageGlyph, attributes: [:])
textView.insertAttributedText(attr)
noppenoppe

自作のheicでNSAdapativeImageGlyphを追加する

let exportURL = URL(filePath: NSTemporaryDirectory()).appending(component: "new.dat")
try? FileManager.default.removeItem(at: exportURL)
let destination = CGImageDestinationCreateWithURL(
        exportURL as CFURL, UTType.heic.identifier as CFString, 1, nil)!

let cgImage = createCGImage(width: 160, height: 160)!
CGImageDestinationAddImage(destination, cgImage, [:] as CFDictionary)

CGImageDestinationFinalize(destination)

let data = try! Data(contentsOf: exportURL)
let adaptiveImageGlyph = NSAdaptiveImageGlyph(imageContent: data)
let attr = NSAttributedString(adaptiveImageGlyph: adaptiveImageGlyph, attributes: [:])
textView.insertAttributedText(attr)
writeImageAtIndex:1025: ⭕️ ERROR: 'Example' is trying to save an opaque image (160x160) with 'AlphaLast'. This would unnecessarily increase the file size and will double (!!!) the required memory when decoding the image --> ignoring alpha.

↑このエラーはCGImageの生成時のものなので関係ない

認識はしないが、insertすると空白の文字が入る。
NSAdaptiveImageGlyphの時点でimageContentが0なので変なデータを入れるとここで弾かれる

noppenoppe

metadataを移植した画像1つのheicはNSAdaptiveImageGlyphとして認識した

let url = Bundle.main.url(forResource: "NSAdaptiveImageGlyphImageContent", withExtension: "heic")!
let data = try! Data(contentsOf: url)
let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil)!
let metadata = CGImageSourceCopyMetadataAtIndex(imageSource, 0, nil)!

let exportURL = URL(filePath: NSTemporaryDirectory()).appending(component: "new.dat")
try? FileManager.default.removeItem(at: exportURL)
let destination = CGImageDestinationCreateWithURL(
        exportURL as CFURL, UTType.heic.identifier as CFString, 1, nil)!

let cgImage = createCGImage(width: 160, height: 160)!
let image = UIImage(cgImage: cgImage)
CGImageDestinationAddImageAndMetadata(destination, cgImage, metadata, nil)
CGImageDestinationFinalize(destination)

let data2 = try! Data(contentsOf: exportURL)
let adaptiveImageGlyph = NSAdaptiveImageGlyph(imageContent: data2)
print(adaptiveImageGlyph.contentIdentifier)
print(adaptiveImageGlyph.contentDescription)
print(adaptiveImageGlyph.imageContent)
let attr = NSAttributedString(adaptiveImageGlyph: adaptiveImageGlyph, attributes: [:])
textView.insertAttributedText(attr)
CE0AF467-2EB5-481E-9B53-DA745BC26BF20
fox glasses
3637 bytes
noppenoppe

自前のmetadataでも成功した。
tiff:DocumentNameさえあれば大丈夫ぽい

func makeMetadata() -> CGImageMetadata {
        let mutableMetadata = CGImageMetadataCreateMutable()
        CGImageMetadataSetValueWithPath(
                mutableMetadata,
                nil,
                "tiff:DocumentName" as CFString,
                NSString(string: "CE0AF467-2EB5-481E-9B53-DA745BC26BF20")
        )
        return mutableMetadata
}
noppenoppe

png画像からカスタムなNSAdaptiveImageGlyphを作る

let imageContent = NSMutableData()
let destination = CGImageDestinationCreateWithData(
    imageContent,
    UTType.heic.identifier as CFString,
    1,
    nil
)!

let url = Bundle.main.url(forResource: "blobcat", withExtension: "png")!
let image = UIImage(contentsOfFile: url.path())!.cgImage!

let metadata = CGImageMetadataCreateMutable()
CGImageMetadataSetValueWithPath(
    metadata,
    nil,
    "tiff:DocumentName" as CFString,
    NSString(string: "8f0ea1a8-704e-495c-beb9-5eb4dcc26783")
)

CGImageDestinationAddImageAndMetadata(destination, image, metadata, nil)
CGImageDestinationFinalize(destination)

let adaptiveImageGlyph = NSAdaptiveImageGlyph(imageContent: imageContent as Data)
let attr = NSAttributedString(adaptiveImageGlyph: adaptiveImageGlyph, attributes: [:])
textView.insertAttributedText(attr)
noppenoppe

通知での活用
INSendMessageIntentでのみ許可されているので、あくまでコミュニケーション用であるっぽい。

noppenoppe

UNNotificationAttributedMessageContextがそもそもちゃんと動いていない気がする

noppenoppe

壊れてなかった、Communication NotificationのCapabilityが必要でした。