How to remove default image icon in NSTextAttachment? - swift

I have an NSAttributedString that can contain NSTextAttachments. I have subclassed the NSTextAttachment and have a property that stores an error message if there was a problem downloading the attachment's image.
I am also subclassing NSLayoutManager to enumerate over attachments and if an error is present in an attachment, it shows the error message on screen in the middle of a CGRect that I provide as a default. In the simulator this works as planned; however, on a device there is a default icon over the bounds of the CGRect. Please see attached images to understand this default icon. How can I override or get rid of this default icon?
Example of the results in the simulator.
Example of the results on a device.
I have researched how to override this icon being displayed but have not found anything. Any ideas?
My initializers in the NSTextAttachment subclass are very basic:
override init(data contentData: Data?, ofType uti: String?) {
super.init(data: contentData, ofType: uti)
}
required convenience init?(coder: NSCoder) {
self.init()
imageCloudID = (coder.decodeObject(forKey: "imageLocation") as! String)
setImage()
}
public override func encode(with coder: NSCoder) {
coder.encode(imageCloudID, forKey: "imageLocation")
}
And then if there is an error downloading the image, it sets the optional error property to that error.
In the NSLayoutManager here is my code:
// Enumerate attachments and draw a placeholder rectangle with error message if an error exists.
textStorage.enumerateAttribute(.attachment, in: textStorage.entireRange, options: []) { optValue, range, stop in
guard let attachment = optValue as? MyImageAttachment else { return }
let glyphRange = self.glyphRange(forCharacterRange: range, actualCharacterRange: nil)
self.enumerateLineFragments(forGlyphRange: glyphRange) { rect, usedRect, textContainer, suppliedGlyphRange, stop in
// Get the size of the rectangle we want to show for the attachment placeholder.
let width = rect.size.width
let imageRect = CGRect(x: rect.minX + 5, y: rect.minY + 10, width: width - 10, height: width / 2)
<<Create a bezier path to outline the rectangle.>>
// Set the UI message to the error message, if present.
guard let imageError = attachment.imageError else {
// If there is not an error present, exit the function.
return
}
let errorMessage: String
switch imageError {
case .no_Internet:
errorMessage = "Internet not available."
<< other error messages >>
}
// Show the error message in the placeholder rectangle.
let messageFont = UIFont.systemFont(ofSize: 16, weight: .light)
let attributedMessage = NSAttributedString(string: errorMessage, attributes: [NSAttributedString.Key.font: messageFont])
// Center the message.
let messageWidth = attributedMessage.size().width
let startingPoint = (imageRect.width - messageWidth) / 2
attributedMessage.draw(in: CGRect(x: startingPoint, y: imageRect.midY, width: imageRect.width, height: imageRect.height))
return
}
}

Recently, I had a similar issue. Assigning nil for image in NSTextAttachment did not work for me.
So this is the workaround I implemented.
let attachment = NSTextAttachment()
attachment.image = UIImage()
return attachment

Related

Full Page screenshot of WKWebView for macOS

This is a macOS app, I'm trying to take a full page screenshot of a webview, but i'm unable to get the screenshot of the full page.
Screenshot function.
func takescreenshot(
_ webView: WKWebView,
didFinish navigation: WKNavigation!) {
let configuration = WKSnapshotConfiguration()
configuration.afterScreenUpdates = true
webView.takeSnapshot(with: configuration) { (image, error) in
if let image = image {
//Save Image
}
}
}
from the answers I've seen here the solution seems to be setting the webview scrollview offset, but this is only available for ios. This is the error i get:
Value of type "WKWebView" has no member "scrollView"
I guess the problem is caused by an internal scroll view that optimizes drawing for scrolling - by redrawing only parts that are soon to be visible.
To overcome this, you can temporarily resize the web view to it's full content extent. I know it's not an ideal solution, but you may derive better one based on this idea.
webView.evaluateJavaScript("[document.body.scrollWidth, document.body.scrollHeight];") { result, error in
if let result = result as? NSArray {
let orig = webView.frame
webView.frame = NSRect(x: 0, y: 0, width: (result[0] as! NSNumber).intValue, height: (result[1] as! NSNumber).intValue)
let configuration = WKSnapshotConfiguration()
webView.takeSnapshot(with: configuration) { image, error in
if let image = image {
NSPasteboard.general.clearContents()
NSPasteboard.general.setData(image.tiffRepresentation, forType: .tiff)
}
}
webView.frame = orig
}
}

How can I put the brand logo in the QR code?

extension CIImage {
/// Combines the current image with the given image centered.
func combined(with image: CIImage) -> CIImage? {
guard let combinedFilter = CIFilter(name: "CISourceOverCompositing") else { return nil }
let centerTransform = CGAffineTransform(translationX: extent.midX - (image.extent.size.width / 2), y: extent.midY - (image.extent.size.height / 2))
combinedFilter.setValue(image.transformed(by: centerTransform), forKey: "inputImage")
combinedFilter.setValue(self, forKey: "inputBackgroundImage")
return combinedFilter.outputImage!
}
}
/// Creates a QR code for the current URL in the given color.
func qrImage(using color: UIColor, logo: UIImage? = nil) -> CIImage? {
let tintedQRImage = qrImage?.tinted(using: color)
guard let logo = logo?.cgImage else {
return tintedQRImage
}
return tintedQRImage?.combined(with: CIImage(cgImage: logo))
}
/// Returns a black and white QR code for this URL.
var qrImage: CIImage? {
guard let qrFilter = CIFilter(name: "CIQRCodeGenerator") else { return nil }
let qrData = absoluteString.data(using: String.Encoding.ascii)
qrFilter.setValue(qrData, forKey: "inputMessage")
let qrTransform = CGAffineTransform(scaleX: 12, y: 12)
return qrFilter.outputImage?.transformed(by: qrTransform)
}
I created the qr code in the color I want, but when I put the logo, the logo comes over the qr code and a very ugly image is formed. As in the first picture.How can I create a space in the middle of the qr and put the logo in that space?
private func configureQrCode() {
let qrCodeColor = UIColor.qrColor
let qrCodeLogo = UIImage(named: "signinlogo")
guard let qrURLImage = URL(string: QRCodeConstants.qrLink)?.qrImage(using: qrCodeColor, logo: qrCodeLogo) else { return }
qrCode.image = UIImage(ciImage: qrURLImage)
}
[![][1]][1]
[![I want it to look like this, but it's like the one above][2]][2]
QR codes have an error correction feature that you can use to your advantage. Give the logo a white background and put it in the center of the QR code, just make sure the logo isn’t too big or the error correction won’t be able to account for it. If you need an example to follow, use this: https://www.avanderlee.com/swift/qr-code-generation-swift/

How do I update a CALayer with a CVPixelBuffer/IOSurface?

I have an IOSurface-backed CVPixelBuffer that is getting updated from an outside source at 30fps. I want to render a preview of the image data in an NSView -- what's the best way for me to do that?
I can directly set the .contents of a CALayer on the view, but that only updates the first time my view updates (or if, say, I resize the view). I've been poring over the docs but I can't figure out the correct invocation of needsDisplay on the layer or view to let the view infrastructure know to refresh itself, especially when updates are coming from outside the view.
Ideally I'd just bind the IOSurface to my layer and any changes I make to it would be propagated, but I'm not sure if that's possible.
class VideoPreviewController: NSViewController, VideoFeedConsumer {
let customLayer : CALayer = CALayer()
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
print("Loaded our video preview")
view.layer?.addSublayer(customLayer)
customLayer.frame = view.frame
// register our view with the browser service
VideoFeedBrowser.instance.registerConsumer(self)
}
override func viewWillDisappear() {
// deregister our view from the video feed
VideoFeedBrowser.instance.deregisterConsumer(self)
super.viewWillDisappear()
}
// This callback gets called at 30fps whenever the pixelbuffer is updated
#objc func updateFrame(pixelBuffer: CVPixelBuffer) {
guard let surface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else {
print("pixelbuffer isn't IOsurface backed! noooooo!")
return;
}
// Try and tell the view to redraw itself with new contents?
// These methods don't work
//self.view.setNeedsDisplay(self.view.visibleRect)
//self.customLayer.setNeedsDisplay()
self.customLayer.contents = surface
}
}
Here's my attempt of a scaling version that's NSView rather than NSViewController-based, that also doesn't update correctly (or scale correctly for that matter):
class VideoPreviewThumbnail: NSView, VideoFeedConsumer {
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
self.wantsLayer = true
// register our view with the browser service
VideoFeedBrowser.instance.registerConsumer(self)
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.wantsLayer = true
// register our view with the browser service
VideoFeedBrowser.instance.registerConsumer(self)
}
deinit{
VideoFeedBrowser.instance.deregisterConsumer(self)
}
override func updateLayer() {
// Do I need to put something here?
print("update layer")
}
#objc
func updateFrame(pixelBuffer: CVPixelBuffer) {
guard let surface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else {
print("pixelbuffer isn't IOsurface backed! noooooo!")
return;
}
self.layer?.contents = surface
self.layer?.transform = CATransform3DMakeScale(
self.frame.width / CGFloat(CVPixelBufferGetWidth(pixelBuffer)),
self.frame.height / CGFloat(CVPixelBufferGetHeight(pixelBuffer)),
CGFloat(1))
}
}
What am I missing?
Maybe I'm wrong, but I think you are you updating your NSView on a background thread. (I suppose that the callback to updateFrame is on a background thread)
If I'm right, when you want to update the NSView, convert your pixelBuffer to whatever you want (NSImage?), and then dispatch it on the main thread.
Pseudocode (I don't work often with CVPixelBuffer so I'm not sure this is the right way to convert to an NSImage)
let ciImage = CIImage(cvImageBuffer: pixelBuffer)
let context = CIContext(options: nil)
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let cgImage = context.createCGImage(ciImage, from: CGRect(x: 0, y: 0, width: width, height: height))
let nsImage = NSImage(cgImage: cgImage, size: CGSize(width: width, height: height))
DispatchQueue.main.async {
// assign the NSImage to your NSView here
}
Another catch: I did some tests, and it seems that you cannot assign an IOSurface directly to the contents of a CALayer.
I tried with this:
let textureImageWidth = 1024
let textureImageHeight = 1024
let macPixelFormatString = "ARGB"
var macPixelFormat: UInt32 = 0
for c in macPixelFormatString.utf8.reversed() {
macPixelFormat *= 256
macPixelFormat += UInt32(c)
}
let ioSurface = IOSurfaceCreate([kIOSurfaceWidth: textureImageWidth,
kIOSurfaceHeight: textureImageHeight,
kIOSurfaceBytesPerElement: 4,
kIOSurfaceBytesPerRow: textureImageWidth * 4,
kIOSurfaceAllocSize: textureImageWidth * textureImageHeight * 4,
kIOSurfacePixelFormat: macPixelFormat] as CFDictionary)!
IOSurfaceLock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
let test = CIImage(ioSurface: ioSurface)
IOSurfaceUnlock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
v1?.layer?.contents = ioSurface
Where v1 is my view. No effect
Even with a CIImage no effect (just last few lines)
IOSurfaceLock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
let test = CIImage(ioSurface: ioSurface)
IOSurfaceUnlock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
v1?.layer?.contents = test
If I create a CGImage it works
IOSurfaceLock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
let test = CIImage(ioSurface: ioSurface)
IOSurfaceUnlock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
let context = CIContext.init()
let img = context.createCGImage(test, from: test.extent)
v1?.layer?.contents = img
I encountered this problem myself and the solution is to double buffer the IOSurface source: use two IOSurface objects instead of one and render to the current surface, set the surface to the layer contents and then on the next rendering pass use the alternate (back/front) surface and then swap.
It would appear that setting the CALayer.contents twice to the same CVPixelBufferRef has no effect. However, if you alternate between two IOSurfaceRef it works wonderfully.
It maybe also possible to invalidate the layer contents by setting it to nil and then reset. I did not try that case but am using the double buffer technique.
If you have some IBActions that update it then create an observed variable with the didSet block and whenever the IBAction is triggered, change its value. Also remember to write the code you want to run when updated in that block.
I'd suggest making the variable an Int, set its default value to 0 and add 1 to it every time it updates.
And you can cast the NSView into an NSImageView for the part where you ask about showing the IMAGE data on an NSView so that does the job.
You need to convert the pixel buffer to CGImage and convert it to a layer so that you can change the layer of the main view.
Please try this code
#objc
func updateFrame(pixelBuffer: CVPixelBuffer) {
guard let surface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else {
print("pixelbuffer isn't IOsurface backed! noooooo!")
return;
}
void *baseAddr = CVPixelBufferGetBaseAddress(pixelBuffer);
size_t width = CVPixelBufferGetWidth(pixelBuffer);
size_t height = CVPixelBufferGetHeight(pixelBuffer);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef cgContext = CGBitmapContextCreate(baseAddr, width, height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), colorSpace, kCGImageAlphaNoneSkipLast);
CGImageRef cgImage = CGBitmapContextCreateImage(cgContext);
CGContextRelease(cgContext);
let outputImage = UIImage(cgImage: outputCGImage, scale: 1, orientation: img.imageOrientation)
let newLayer:CGLayer = CGLayer.init(cgImage: outputImage)
self.layer = newLayer
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
CVPixelBufferRelease(pixelBuffer);
}

How to name UIImage drawn from UIBezierPath() programmatically in Xcode-Swift?

How do we name an image programmatically. For example, assign a name to the image generated below. A name that we can use to distinguish the image from other images drawn programmatically.
func drawOval (width: CGFloat, height: CGFloat, name: String) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: width, height: height))
let image = renderer.image { ctx in
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: width, height: height))
path.stroke()
}
// TO DO: Assign this image a name, for example "image01"
return image
}
You can use tags on each UIImageView. I’m not aware of a way to add an identifier to a UIImage directly since it is a subclass of NSObject and not UIView. In order to add a tag to an object in Swift, the object must be a view of some kind.
To implement this, you would keep a variable outside of that function that keeps track of the current tag, then increment it in your function. For example:
var currentTag = 0
//Function now returns a UIImageView
func drawOval (width: CGFloat, height: CGFloat, name: String) -> UIImageView {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: width, height: height))
let image = renderer.image { ctx in
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: width, height: height))
path.stroke()
}
let imageView = UIImageView(image: image)
imageView.tag = currentTag
currentTag += 1
return imageView
}
Then later in your code:
if (imageView.tag == 0) {
//Do something
}
//You can also use
let taggedImageView = viewWithTag(0)
EDIT: If you want to save the images and load them via one of the available UIImage initializers, you can write them to a cache folder on disk, then retrieve them using UIImage(pathToFile:):
//This will store the images in the caches directory for your app, which
//the system can clear when the device is low on storage. It will not be
//cleared while your app is open, though.
func saveImageToCacheDynamically(image: UIImage, name: String) {
let paths = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
let localPath = paths[0].appendingPathComponent(“ImageCache”, isDirectory: true)
do {
if FileManager.default.fileExists(atPath: localPath.absoluteString) {
//Write the png data representation of the image to disk in plaintext format
try image.pngData().write(to: localPath.appendingPathComponent("\(name).txt"))
} else {
try FileManager.default.createDirectory(at: localPath, withIntermediateDirectories: true, attributes: nil)
//Write the png data representation of the image to disk in plaintext format
try image.pngData().write(to: localPath.appendingPathComponent("\(name).txt"))
}
} catch {
print("Error locally saving image: \(error.localizedDescription)")
}
//Later in your code...
let paths = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
let localPath = paths[0].appendingPathComponent("ImageCache/\(imageIdentifier).txt")
if FileManager.default.fileExists(atPath: localPath.absoluteString) {
var fileData: Data!
do {
try fileData = Data(contentsOf: localPath)
} catch {
print("Error reading image file: \(error.localizedDescription)")
}
let image = UIImage(data: fileData)
//Do something with image
} else {
print("Error: image does not exist")
}
I believe what you are looking for is something like NSCache...
You can define a cache by something similar to this:
let imageCache = NSCache<String, UIImage>()
Then you can add objects to the cache like this, where someKeyString is the 'name' you are referring to:
imageCache.setObject(someImage, forKey: someKeyString)
And then finally you can retrieve images from the cache like
imageCache.object(forKey: someKeyString)
I would recommend using extensions or something similar to maintain a reference to your cache everywhere in your app.
** NOTE:
NSCaches are cleared when memory space is short, your app closes, etc. See here
For more permanent storage, I would recommend using UserDefaults, which Apple describes as "An interface to the user’s defaults database, where you store key-value pairs persistently across launches of your app." Use this for things like profile images or things that won't change very often. I would also recommend looking into Core Data

IMessage MSSticker view created from UIView incorrect sizing

Hey I have been struggling with this for a couple of days now and can't seem to find any documentation out side of the standard grid views for MSStickerView sizes
I am working on an app that creates MSStickerViews dynamically - it does this via converting a UIView into an UIImage saving this to disk then passing the URL to MSSticker before creating the MSStickerView the frame of this is then set to the size of the original view.
The problem I have is that when I drag the MSStickerView into the messages window, the MSStickerView shrinks while being dragged - then when dropped in the messages window, changes to a larger size. I have no idea how to control the size when dragged or the final image size
Heres my code to create an image from a view
extension UIView {
func imageFromView() -> UIImage? {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.isOpaque, 0.0)
defer { UIGraphicsEndImageContext() }
if let context = UIGraphicsGetCurrentContext() {
self.layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}
return nil
}
}
And here's the code to save this to disk
extension UIImage {
func savedPath(name: String) -> URL{
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let filePath = "\(paths[0])/name.png"
let url = URL(fileURLWithPath: filePath)
// Save image.
if let data = self.pngData() {
do {
try data.write(to: url)
} catch let error as NSError {
}
}
return url
}
}
finally here is the code that converts the data path to a Sticker
if let stickerImage = backgroundBox.imageFromView() {
let url = stickerImage.savedPath(name: textBox.text ?? "StickerMCSticker")
if let msSticker = try? MSSticker(contentsOfFileURL: url, localizedDescription: "") {
var newFrame = self.backgroundBox.frame
newFrame.size.width = newFrame.size.width
newFrame.size.height = newFrame.size.height
let stickerView = MSStickerView(frame: newFrame, sticker: msSticker)
self.view.addSubview(stickerView)
print("** sticker frame \(stickerView.frame)")
self.sticker = stickerView
}
}
I wondered first off if there was something I need to do regarding retina sizes, but adding #2x in the file just breaks the image - so am stuck on this - the WWDC sessions seem to show stickers being created from file paths and not altering in size in the transition between drag and drop - any help would be appreciated!
I fixed this issue eventually by getting the frame from the view I was copying's frame then calling sizeToFit()-
init(sticker: MSSticker, size: CGSize) {
let stickerFrame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
self.sticker = MSStickerView(frame: stickerFrame, sticker: sticker)
self.sticker.sizeToFit()
super.init(nibName: nil, bund
as the StickerView was not setting the correct size. Essentially the experience I was seeing was that the sticker size on my view was not accurate with the size of the MSSticker - so the moment the drag was initialized, the real sticker size was implemented (which was different to the frame size / autoLayout I was applying in my view)