用NSString替代Photoshop (2015)

Lobsters Hottest 工具

摘要

作者讨论了在Cocoa中使用NSString和CoreGraphics以编程方式绘制简单矢量图形来替代Photoshop的方法,阐述了其中的好处与挑战,同时推广了自己开发的app Findings。

<p><a href="https://lobste.rs/s/uuecsm/replacing_photoshop_with_nsstring_2015">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/29 07:54

# 用 NSString 替换 Photoshop —— Cocoa 矿洞里的实验鼠 来源:http://cocoamine.net/blog/2015/03/20/replacing-photoshop-with-nsstring/ 你好!这篇帖子意外获得了大量关注。感谢来访!如果你喜欢它,希望你能看看我的应用 Findings(http://findingsapp.com/),一款面向科学家和研究人员的实验记录应用,并让更多人知道它。 一款应用不仅仅由代码构成。它还包含静态资源,如图片和声音。图片通常使用专用工具创建和编辑,比如 Acorn(http://www.flyingmeat.com/acorn/)(我的最爱)、Pixelmator(http://www.pixelmator.com/)或是那个 800 磅重的大猩猩——Photoshop(http://www.photoshop.com/)。理想情况下,图形处理应交给实际的设计师(https://twitter.com/wrinklypea),这确实是我们为 Findings(http://findingsapp.com/)做的最棒的事情之一。但作为开发者,当你只需要一个由几条直线、正方形或圆形构成的简单小图标时,却要使用单独的工具或请别人帮忙,这有点繁琐。由于“视网膜”屏,你还得为同一幅绘图创建 1 倍、2 倍、甚至现在的 3 倍缩放版本。任何微小的改动或添加小变体,很快就会变成一件繁琐且易出错的事。 我是个程序员,我肯定能用代码画出来! 开发者该怎么办?写代码!我不记得第一次决定直接用代码绘制图像是什么时候了,但当时那似乎是个好主意。从开发者的角度来看,这非常有诱惑力。当你拥有最灵活的工具——代码时,为什么还要用 Photoshop?Photoshop 本身也是用代码写的,所以 Photoshop 能做到的,代码也能做到!可惜在实际中,这只对非常简单的图形才合理。即便如此,这也并非直截了当的任务,而且远没有我天真期待的那样有趣。我将首先向你展示一个例子,说明它的复杂性,但别担心,之后我也有一个有趣的替代方案。 ### 代码太多 如承诺所示,下面是我早期用 Objective C 绘制图像的一个例子。做好准备: ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ``` ``` // chevron is defined by 3 points, the angle is always 90 degrees // // A // # // # // B // # // # // C CGFloat rightMargin = 12.0; CGFloat chevronHeight = 9.0; // then chevronWidth = chevronHeight/2 CGFloat lineWidth = 2.0; NSRect bounds = [self bounds]; NSPoint middle = NSMakePoint(NSMaxX(bounds)-rightMargin-lineWidth/2.0, (NSMinY(bounds)+NSMaxY(bounds))/2.0); NSPoint top = middle; top.x -= chevronHeight/2.0; top.y += chevronHeight/2.0; NSPoint bottom = top; bottom.y -= chevronHeight; // draw the chevron in grey NSBezierPath *chevronPath = [NSBezierPath bezierPath]; [chevronPath setLineWidth:lineWidth]; [chevronPath setLineJoinStyle:NSMiterLineJoinStyle]; [chevronPath setLineCapStyle:NSButtLineCapStyle]; [chevronPath moveToPoint:top]; [chevronPath lineToPoint:middle]; [chevronPath lineToPoint:bottom]; NSColor *chevronColor = [NSColor colorWithCalibratedWhite:0.4 alpha:1.0]; [chevronColor set]; [chevronPath stroke]; ``` 哇,仅仅画两条 90 度角的线就用了**这么多代码**!这还没算上实际的 NSImage 代码。好处是我可以轻松改变颜色和大小,并且一次性得到 1x、2x 和 3x 版本。但所有这些代码真的值吗?经过第一次体验后,我并没有被打动,但还是又在几次需要非常简单的图形时使用了这种方法。随着经验增长,它变得稍微容易些,投入的时间也得到了回报,但我仍然对这种情况感到沮丧。不过,过了一段时间,我意识到代码中最有趣的部分实际上是我用作绘图指南的 ASCII 艺术: ``` 1 2 3 4 5 6 7 ``` ``` // A // # // # // B <-- 我只想写这个, // # 而不是剩下的代码! // # // C ``` 这种“绘图”很好地描述了我想要做的事情,实际上比我能为任何代码写的任何注释都要好。这个 ASCII 艺术是一种很好的方式,可以直接在代码中展示 UI 那部分会用到什么图像,而无需深入资源文件夹。实际的绘图代码突然显得多余了。如果我能直接把 ASCII 艺术传递给 NSImage 呢? ### ASCIImage:结合 ASCII 艺术和幼儿园技能 Xcode 不能编译 ASCII 艺术,所以我决定自己写一个必要的“ASCII 艺术编译器”。好吧,我没写编译器,而是写了一个叫“ASCIImage”的小项目!它作为 UIImage/NSImage 的一个类别,提供几个工厂方法,适用于 iOS 和 Mac。它是开源的,以 MIT 许可证发布在 GitHub(https://github.com/cparnot/ASCIImage)上。我还建立了一个登陆页面,链接到由 @mz2(http://twitter.com/mz2)在 NSConference 期间仅用几个小时拼凑出来的编辑器:asciimage.org(http://asciimage.org/)。 它非常容易使用,功能有限。不过,它不仅仅是一个玩具项目。过去一年,我一直在一个真实的应用——Findings(http://findingsapp.com/)中使用它。但无论如何,这里有一个好的经验法则:一旦你感到受限于它,就应该改用 Acorn(http://www.flyingmeat.com/acorn/),或者更好的是,联系设计师(https://twitter.com/wrinklypea)。 下面是使用 ASCIImage 绘制一个 2 点粗的 V 形图标的方法: ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ``` ``` + (UIImage *)chevronImageWithColor:(UIColor *)color { NSArray *asciiRep = @[ @"· · · · · · · · · · · ·", @"· · · 1 2 · · · · · · ·", @"· · · A # # · · · · · ·", @"· · · · # # # · · · · ·", @"· · · · · # # # · · · ·", @"· · · · · · 9 # 3 · · ·", @"· · · · · · 8 # 4 · · ·", @"· · · · · # # # · · · ·", @"· · · · # # # · · · · ·", @"· · · 7 # # · · · · · ·", @"· · · 6 5 · · · · · · ·", @"· · · · · · · · · · · ·", ]; return [self imageWithASCIIRepresentation:asciiRep color:[UIColor blackColor] shouldAntialias:NO]; } ``` 下面是根据绘图环境生成的图像: 从 chevron ASCII 艺术得到的 ASCIImage 结果 在 iOS 上,1x/2x/3x 版本将根据应用运行设备的屏幕分辨率生成。在 Mac 上,ASCIImage 实现使用 NSImage 的块 API,这意味着在图像渲染到屏幕时,将以正确的分辨率进行绘制。注意,我在示例代码中禁用了抗锯齿(因此只按需生成顶行的图像)。对于这种形状,禁用抗锯齿的渲染实际上更清晰、效果更好。 在幕后,ASCIImage 做的事情简单而枯燥。可能有办法让解析更智能和用户友好,但我只想快速工作,而不想过多纠结、编码和调试: - 它会移除所有空白;这就是为什么所有像素都需要以某种方式标记(我在上面的示例中选择字符 '·' 作为背景); - 检查一致性:所有行的长度应相同; - 解析字符串以找到数字和字母;其他所有内容都被忽略,即示例中的 '·' 和 '#' 字符; - 每个数字/字母对应一个 NSPoint; - 根据你在幼儿园学到的“连线”技术创建形状; - 每个形状被转换为 NSBezierPath; - 每个路径用正确的颜色和抗锯齿标志渲染 在 V 形示例中,只有一个形状,它按如下方式创建和渲染: ASCIImage 渲染步骤 ### 基础 这里是 ASCIImage 使用方法的快速概览。用于连线的有效字符,**按此顺序**: ``` 1 2 3 4 5 6 7 8 9 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n p q r s t u v w x y z ``` 每个形状由一系列连续的字符定义,一旦你在上面的列表中跳过一个字符,就开始一个新的形状。因此,第一个形状可以由序列 '123456' 定义,下一个形状由 '89ABCDEF' 定义,再下一个由 'HIJKLMNOP' 定义,等等。最简单的方法 `\+imageWithASCIIRepresentation:color:shouldAntialias:` 会用传入的颜色绘制并填充每个形状(还有一个基于块的方法用于更多选项)。以下是一个包含 3 个形状的例子: 包含 3 个形状的 ASCIImage 示例 你也可以通过使用相同的字符两次来绘制直线。在这种情况下,你不需要在下一个形状或线之前跳过字符。这里是一个包含多条线的例子(记住,'#' 只在你查看代码时作为视觉指南,但会被 ASCIImage 的解析器忽略): 包含多条线的 ASCIImage 示例 当然,你也可以组合形状和线: 组合形状和线的 ASCIImage 示例 只有两个特殊的情况。如果你使用一个孤立的字符,可以创建一个(方形的)像素。如果你使用相同的字符三次或更多次,可以绘制一个椭圆。椭圆将由这些点所包围的最大矩形定义。如果矩形是正方形,椭圆就是圆: 包含椭圆的 ASCIImage 示例 最后,一个更复杂的组合,展示你能用它做到什么程度。这个特定的 ASCII 艺术已经进入了混淆领域,这显然违背了初衷。不过,乐趣还在! 绘制一个 bug 的复杂 ASCIImage 示例 基础部分就这些! ### 附加功能 ASCIImage 中定义了第二个工厂方法: ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ``` ``` /// This method offers more advanced options that can // be set on each "shape", using the `contextHandler` block. // The mutable dictionary passed by the block can be modified // using the keys listed in the constants below. The dictionary // initially contains the `ASCIIContextShapeIndex` key to // signal which shape the context will be applied to. + (PARImage *)imageWithASCIIRepresentation:(NSArray *)rep contextHandler:(void(^)(NSMutableDictionary *ctx))handler; /// keys for the dictionary context extern NSString * const ASCIIContextShapeIndex; extern NSString * const ASCIIContextFillColor; extern NSString * const ASCIIContextStrokeColor; extern NSString * const ASCIIContextLineWidth; extern NSString * const ASCIIContextShouldClose; extern NSString * const ASCIIContextShouldAntialias; ``` 这个方法允许你对图形中每个元素的绘制应用不同的设置。这是通过一个可变的字典作为块的参数来实现的。信息双向流动:从 ASCIImage 到你,然后从你到 ASCIImage。你会得到形状的索引(基于 ASCII 艺术中使用的字符排序),然后你可以设置描边颜色、填充颜色、抗锯齿标志等。注意,这个上下文与实际的 `NSGraphicsContext` 没有太多共同点。它非常有限,而且不幸的是,对于 ASCIImage 需要进行的绘制,直接操作 `NSGraphicsContext` 是不可行的(至少存在足够的陷阱,以至于我决定不这样做)。 下面是一个例子,展示如何使用基于块的方法将多个形状层叠在一起: ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 ``` ``` - (NSImage *)deletionImage { NSArray *asciiRep = @[ @"· · · · 1 1 1 · · · ·", @"· · 1 · · · · · 1 · ·", @"· 1 · · · · · · · 1 ·", @"1 · · 2 · · · 3 · · 1", @"1 · · · # · # · · · 1", @"1 · · · · # · · · · 1", @"1 · · · # · # · · · 1", @"1 · · 3 · · · 2 · · 1", @"· 1 · · · · · · · 1 ·", @"· · 1 · · · · · 1 · ·", @"· · · 1 1 1 1 1 · · ·", ]; return [NSImage imageWithASCIIRepresentation:asciiRep contextHandler:^(NSMutableDictionary *context) { NSInteger index = [context[ASCIIContextShapeIndex] integerValue]; if (index == 0) { context[ASCIIContextFillColor] = [NSColor grayColor]; } else { context[ASCIIContextLineWidth] = @(1.0); context[ASCIIContextStrokeColor] = [NSColor whiteColor]; } context[ASCIIContextShouldAntialias] = @(YES); }]; } ``` 结果如下: 在灰色圆形中绘制白色十字的 ASCIImage,使用不同颜色的层叠形状 现在这个例子将 ASCIImage 推向了极限,但进一步展示了如何利用基本形状的层叠来创建更复杂的图标: ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 ``` ``` - (PARImage *)lockImage { NSArray *asciiRep = @[ @" · · · · · · · · · · · · · · · ", @" · · · · 1 · · · · · · 1 · · · ", @" · · · · · · · · · · · · · · · ", @" · · · · · · · · · · · · · · · ", @" · · · · · · · · · · · · · · · ", @" · · 3 · 1 · · · · · · 1 · 4 · ", @" · · · · · · · · · · · · · · · ", @" · · · · · · A · · A · · · · · ", @" · · · · 1 · · · · · · 1 · · · ", @" · · · · · · · C D · · · · · · ", @" · · · · · · A · · A · · · · · ", @" · · · · · · · · · · · · · · · ", @" · · · · · · · B E · · · · · · ", @" · · · · · · · · · · · · · · · ", @" · · 6 · · · · · · · · · · 5 · ", ]; return [PARImage imageWithASCIIRepresentation:asciiRep contextHandler:^(NSMutableDictionary *context) { NSInteger index = [context[ASCIIContextShapeIndex] integerValue]; if (index == 0) { context[ASCIIContextFillColor] = [PARColor blackColor]; } else { context[ASCIIContextFillColor] = [PARColor whiteColor]; } context[ASCIIContextShouldClose] = @(YES); context[ASCIIContextShouldAntialias] = @(YES); }]; } ``` ASCII 艺术混淆!方法名出卖了它。有点吧。这是字符串如何被一个形状一个形状、一层一层地解析: 使用不同颜色的多层形状绘制锁的 ASCIImage 同样,不确定你是否想走这么远,但现在你知道你可以做到! ### 棘手的地方 实现 ASCIImage 非常直接,但仍有几个棘手的地方: - “填充”一个形状实际上需要对 NSBezierPath 同时进行 `fill` 和 `stroke`。为了正确填充像素并实现像素对齐,定义每个贝塞尔路径的顶点实际上被设置在 ASCII 艺术所表示的 1x1 点“像素”的中间(例如,1x1 点在 3x 缩放中最终是 3x3 像素)。当填充路径时,贝塞尔路径的边缘因此会在实际边界外半个点处绘制。然后我们还需要用相同的颜色应用宽度为 1 点的描边,以填充完整的预期形状。 为了真正填充,你需要填充... 并描边。 - 在没有抗锯齿的情况下,要正确地将像素变黑是很棘手的。为此,我发现应该对 45 度线使用更粗的线宽,等于 1 点正方形的对角线:2 的平方根。这个宽度对于其他角度,包括水平和垂直线也有效,因此在锯齿渲染中使用这个宽度来绘制线条,而不是抗锯齿渲染中使用的 1 点宽度。 - 对于测试,你需要让系统相信缩放是 1x、2x 或 3x。在 iOS 上,ASCIImage 有一个带缩放参数的特殊方法,实际实现也会使用它(简单地传递当前设备的缩放),确保正确的缩放行为。

相似文章

全程原生,直到你需要文本

Hacker News Top

一位资深 macOS/iOS 开发者讲述了使用苹果原生框架(SwiftUI、AppKit、TextKit)实现支持 Markdown 的聊天界面的挣扎,最终发现像 Electron 这样的基于 Web 的技术为富文本渲染提供了更实用的解决方案。

Apple的Image Playground不再糟糕了

TechCrunch AI

Apple在WWDC 2026上宣布对其Image Playground AI图像生成工具进行重大改进,包括更高质量的输出、自然语言提示、更深度的系统集成,同时强调隐私保护。

@PrajwalTomar_: 我在不到一小时内重新设计了整个客户项目。专业排版。一致的颜色系统。设计语言真正统一。没有设计师。没有Figma。没有8000美元的发票。只有Claude Code + Stitch 2.0通过MCP。如果你的AI构建的应用看起来很糟糕,那不是AI的问题,而是工作流程的问题。本文详细解析了我几周来一直使用的确切工作流程:

X AI KOLs Following

一位开发者分享了使用Claude Code和Stitch 2.0(通过MCP)快速重新设计客户项目的工作流程,无需传统设计工具或设计师。