我开发的 Mac 看图软件 Tovi, 支持播放 GIF 动画, 以及用箭头按键浏览上一张下一张. 支持缩放, 旋转, 导出成 mp4 视频等等. Tovi 曾经作为收费软件在苹果 App Store 上售卖, 现在, Tovi 已经免费了.
下载地址: http://tovi.ideawu.com/
我开发的 Mac 看图软件 Tovi, 支持播放 GIF 动画, 以及用箭头按键浏览上一张下一张. 支持缩放, 旋转, 导出成 mp4 视频等等. Tovi 曾经作为收费软件在苹果 App Store 上售卖, 现在, Tovi 已经免费了.
下载地址: http://tovi.ideawu.com/
现代计算机系统在进行图像处理时,可以利用 CPU 或者显卡两种芯片之一进行处理,也可以同时使用两者。本文讨论在 macOS 上的图像处理。
1. 完全使用 CPU 进行图像处理
图像(如 png)从硬盘中读取并解压到内存中,之后的图像处理完全用 CPU 操作内存,和显卡无关。
2. 完全使用显卡进行图像处理
图像(如 png)从硬盘中读取并解压到内存中,然后传到显卡显存中(创建 OpenGL texture),图像一旦传到显卡,内存中的图像数据就可以删除了。
3. 同时使用 CPU 和显卡进行图像处理
图像同时存于内存以及显存中,有时利用显卡进行处理,有时利用 CPU 进行处理。因为图像数据同时保存于内存和显存中,所以需要某种绑定机制,关联内存中的图像和显存中的图像,并在一方变更时,更新另一方。
# CVPixelBufferRef 与 CVOpenGLTextureRef
CVPixelBufferRef 表示内存中的图像。
CVOpenGLTextureRef 表示显存中的图像,内部使用 OpenGL 的 texture。
苹果的 Core Video 框架提供了显存图像到内存图像的单向实时绑定机制,也即绑定 CVPixelBufferRef 和 CVOpenGLTextureRef,当后者(显存)更新时,框架自动更新前者(内存)。注意,这种实时绑定是单向的,更新的传导只有一个方向,即从显存到内存。
使用 CVOpenGLTextureCacheCreateTextureFromImage() 函数来建立这种绑定关系。绑定建立后,用户(也即你的代码)对显存或者内存的操作,都必须涉及到锁,因为框架本身同时也会操作这两份数据。
涉及到多者同时操作一份数据的情况,都需要锁。
使用 CVPixelBufferLockBaseAddress() 和 CVPixelBufferUnlockBaseAddress() 两个函数进行锁操作。显然,当你的代码要操作内存数据时,你应该 lock CVPixelBufferRef,当你想操作显存数据时,就应该 unlock CVPixelBufferRef。
前面提到,这种单向绑定会将显存的更新传导到内存,所以,当你更新完显存(即执行 OpenGL glFinish 操作,文档提到是 glFlush 之后)之后,你就获得了 OpenGL 渲染的图像数据(OpenGL 截图),其内部实现应该是用 glReadPixels() 或者 glGetTexImage() 函数。这时,你就可以把 OpenGL 渲染的结果保存成 png 文件了。
# 关于 Metal 框架的 CVMetalTextureRef
CVMetalTextureRef 是用来替代 CVPixelBufferRef 的,因为苹果已经发布了 Metal 框架用来替代所有苹果操作系统上的 OpenGL。
提一句,从面向对象的角度 CVPixelBufferRef CVOpenGLTextureRef CVMetalTextureRef 这三者都是 CVImageBufferRef 的子类。不过,在 C 语言里,这三者是同一个结构体,只不过用 typedef 命了 4 个名字而已。
typedef CVImageBufferRef CVPixelBufferRef; typedef CVImageBufferRef CVOpenGLTextureRef; typedef CVImageBufferRef CVMetalTextureRef;
CVImageBufferRef(也即 CVBufferRef)内部应该是用了 type 字段来表示不同的子类,毕竟 C 语言没有 C++ 那样的类和继承,只能用结构体来实现子类,都是惯用法,大同小异。
如果你使用 Core Graphics, 那么从内存到显存的路径是这样的: CGImage => CGBitmapContext => OpenGL texture
在至少两台这个显卡的2018款 MacBook Pro 15 寸电脑上已经测试发现,苹果用的最高性能的 AMD Radeon Pro Vega 20 显卡可能有驱动问题,或者硬件问题,会导致系统在 1440x900 分辨率下出现闪屏(瞬间花屏)的问题。
这个闪屏问题很容易复现:
* 把分辨率从默认的 1680x1050 改成 1440x900
* 运行 iMovie,或者 Chess 等等使用高性能显卡的应用
* 打开 App Store,搜索 iMovie 或者别的软件,然后播放软件的介绍视频
这时你应该能发现屏幕隔几秒就花屏闪一次。如果不出现,你可以试着锁屏,等屏幕黑了之后,再登录系统,确保视频在播放着。
macOS 实现 drag and drop 拖放很复杂,因为苹果的开发者中心把很多 demo 代码都不知道放到哪里去了,你用苹果开发者网站找不到 Sample Code,用 Xcode 帮助也没法找到(补充:sample code 在 https://developer.apple.com/library/archive/navigation/#section=Resource%20Types&topic=Sample%20Code )。而通过搜索引擎找的话,各种例子不是老旧就是没讲对。
其实 drag and drop 很简单。有两个概念:DraggingSource 和 DraggingDestination。你可以把拖放理解两个容器之间的交互,你要将某物从A拖到B然后放下。
最重要的一点,不管实现什么接口什么方法,你必须告诉系统什么时候拖放开始了。我看了很多文章,都没有重点提到这一点。也就是说,不管你实现什么接口都没有用,你必须主动告诉系统拖放已经开始了。所以,在你的代码中明确地告诉系统这一点,一般是鼠标按住的时候。
当你决定拖放已经开始了,就调用这个方法:
// NSView 的方法,当你决定 drag-n-drop 可以开始的时候,调用此方法 - (NSDraggingSession *)beginDraggingSessionWithItems:(NSArray*)items event:(NSEvent *)event source:(id )source;
调用这个方法之后,系统会自动地更新拖动过程的示意图的位置。当然,你要告诉系统示意图的开始位置和图片,通过上面的方法中的参数。另外,drag-n-drop 通过剪贴板在源和目的之间进行通信,所以剪贴板充当通信信道。下面的代码配置了通信信道和示意图:
// NSPasteboardItem 用于在 drag-n-drap 的双方之间进行通信 NSPasteboardItem *pbItem = [NSPasteboardItem new]; // 你可以直接传输数据, type 可自定义. [pbItem setString:@"Transition" forType:NSPasteboardTypeString]; // 或者指定传输的数据由 NSPasteboardItemDataProvider 提供。 //[pbItem setDataProvider:self forTypes:@[NSPasteboardTypeString]]; // NSDraggingItem 用于显示 drag-n-drop 过程的示意图 NSDraggingItem *dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter:pbItem]; NSImage *img = [NSImage imageNamed:NSImageNameHomeTemplate]; NSRect frame = NSMakeRect(0, 0, img.size.width, img.size.height); // draggingFrame 用于指定示意图的初始位置(在当前 NSView 中),contents 是示意图(NSImage) [dragItem setDraggingFrame:frame contents:img];
这认为用网络通信模型来理解会更好,网络和通信是一个广义的概念,可以解释很多东西,我之前有文章介绍过:http://www.ideawu.net/blog/archives/528.html。
弄明白了上面的之后,你再查看这几个接口就懂了:NSDraggingSource, NSDraggingDestination, NSPasteboardItemDataProvider
如果你的 Xcode 程序编译正常,但执行时遇到类似的报错:
+[NSCharacterSet SVGWhitespaceCharacterSet]: unrecognized selector sent to class
这种问题是因为静态链接库中的 Objective-C 的 Category 符号没有被链接进你的程序。你可以用下面的命令看看符号有没有被链接:
nm Cli | grep SVGWhitespaceCharacterSet
网上说你可以在 Other Linker Flags
里增加 -ObjC
,但如果你的项目还要用别的非 OC 的库,则会报错,例如你同时使用 C++ 库。正确的做法是,在 Other Linker Flags
里增加
-force_load $(PROJECT_DIR)/...../libxxx.a
-force_load
后面跟绝对路径。
把 /usr/include/libxml2 添加到 header search path