Skip to content

Commit 0fa7101

Browse files
committed
[PlaygroundLogger] Implemented support for logging images, views, and sprites.
We now send PNG data for images and views, plus whatever SpriteKit gives us via _copyImageData. Note that this does not yet handle recursive calls correctly, so in a playground environment this would likely try to log `self` when implicitly calling `-drawRect:` on a view that's being logged.
1 parent 7079964 commit 0fa7101

File tree

8 files changed

+151
-36
lines changed

8 files changed

+151
-36
lines changed

PlaygroundLogger/PlaygroundLogger/CustomLoggable/SpriteKit/SpriteKitOpaqueLoggable.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2017 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2017-2018 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -23,13 +23,11 @@ fileprivate protocol SpriteKitOpaqueLoggable: class, OpaqueImageRepresentable, C
2323
extension SpriteKitOpaqueLoggable {
2424
func encodeImage(into encoder: LogEncoder, withFormat format: LogEncoder.Format) {
2525
guard let copyImageDataMethod = (self as AnyObject)._copyImageData, let imageData = copyImageDataMethod() else {
26-
// TODO: don't crash in this case
27-
fatalError("Unable to get image data, unable to encode anything")
26+
unimplemented("Handle case where we don't have any image data")
2827
}
2928

30-
_ = imageData
31-
32-
unimplemented()
29+
encoder.encode(number: UInt64(imageData.count))
30+
encoder.encode(data: imageData)
3331
}
3432

3533
var opaqueRepresentation: LogEntry.OpaqueRepresentation {

PlaygroundLogger/PlaygroundLogger/OpaqueRepresentations/AppKit/NSBitmapImageRep+OpaqueImageRepresentable.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2017 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2017-2018 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -15,7 +15,12 @@
1515

1616
extension NSBitmapImageRep: OpaqueImageRepresentable {
1717
func encodeImage(into encoder: LogEncoder, withFormat format: LogEncoder.Format) {
18-
unimplemented()
18+
guard let pngData = self.representation(using: .png, properties: [:]) else {
19+
unimplemented("Need to handle error when we couldn't generate PNG data")
20+
}
21+
22+
encoder.encode(number: UInt64(pngData.count))
23+
encoder.encode(data: pngData)
1924
}
2025
}
2126
#endif

PlaygroundLogger/PlaygroundLogger/OpaqueRepresentations/AppKit/NSImage+OpaqueImageRepresentable.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2017 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2017-2018 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -14,8 +14,30 @@
1414
import AppKit
1515

1616
extension NSImage: OpaqueImageRepresentable {
17+
private var bestBitmapRepresentation: NSBitmapImageRep? {
18+
guard let bestRep = self.bestRepresentation(for: NSRect(origin: .zero, size: size).integral, context: nil, hints: nil) else {
19+
// We don't have a best representation, so we can't convert it to a bitmap image rep.
20+
return nil
21+
}
22+
23+
if let bitmapRep = bestRep as? NSBitmapImageRep {
24+
return bitmapRep
25+
}
26+
else {
27+
guard let cgImage = bestRep.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
28+
return nil
29+
}
30+
31+
return NSBitmapImageRep(cgImage: cgImage)
32+
}
33+
}
34+
1735
func encodeImage(into encoder: LogEncoder, withFormat format: LogEncoder.Format) {
18-
unimplemented()
36+
guard let bitmapRep = self.bestBitmapRepresentation else {
37+
unimplemented("Need to figure out how to handle when we can't get the best bitmap rep!")
38+
}
39+
40+
bitmapRep.encodeImage(into: encoder, withFormat: format)
1941
}
2042
}
2143
#endif

PlaygroundLogger/PlaygroundLogger/OpaqueRepresentations/AppKit/NSView+OpaqueImageRepresentable.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2017 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2017-2018 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -15,7 +15,13 @@
1515

1616
extension NSView: OpaqueImageRepresentable {
1717
func encodeImage(into encoder: LogEncoder, withFormat format: LogEncoder.Format) {
18-
unimplemented()
18+
guard let bitmapRep = self.bitmapImageRepForCachingDisplay(in: self.bounds) else {
19+
unimplemented("Need to handle cases where we can't get a bitmap rep!")
20+
}
21+
22+
self.cacheDisplay(in: self.bounds, to: bitmapRep)
23+
24+
bitmapRep.encodeImage(into: encoder, withFormat: format)
1925
}
2026
}
2127
#endif

PlaygroundLogger/PlaygroundLogger/OpaqueRepresentations/CoreGraphics/CGImage+OpaqueImageRepresentable.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2017 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2017-2018 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -12,8 +12,21 @@
1212

1313
import CoreGraphics
1414

15+
#if os(macOS)
16+
import AppKit
17+
#elseif os(iOS) || os(tvOS)
18+
import UIKit
19+
#endif
20+
1521
extension CGImage: OpaqueImageRepresentable {
1622
func encodeImage(into encoder: LogEncoder, withFormat format: LogEncoder.Format) {
17-
unimplemented()
23+
#if os(macOS)
24+
// On macOS, simply create an NSBitmapImageRep with the receiver and use that.
25+
let bitmapRep = NSBitmapImageRep(cgImage: self)
26+
bitmapRep.encodeImage(into: encoder, withFormat: format)
27+
#elseif os(iOS) || os(tvOS)
28+
let uiImage = UIImage(cgImage: self)
29+
uiImage.encodeImage(into: encoder, withFormat: format)
30+
#endif
1831
}
1932
}

PlaygroundLogger/PlaygroundLogger/OpaqueRepresentations/UIKit/UIImage+OpaqueImageRepresentable.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2017 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2017-2018 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -15,7 +15,12 @@
1515

1616
extension UIImage: OpaqueImageRepresentable {
1717
func encodeImage(into encoder: LogEncoder, withFormat format: LogEncoder.Format) {
18-
unimplemented()
18+
guard let pngData = UIImagePNGRepresentation(self) else {
19+
unimplemented("Need to handle when we can't convert a UIImage to a PNG")
20+
}
21+
22+
encoder.encode(number: UInt64(pngData.count))
23+
encoder.encode(data: pngData)
1924
}
2025
}
2126
#endif

PlaygroundLogger/PlaygroundLogger/OpaqueRepresentations/UIKit/UIView+OpaqueImageRepresentable.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2017 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2017-2018 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -15,7 +15,13 @@
1515

1616
extension UIView: OpaqueImageRepresentable {
1717
func encodeImage(into encoder: LogEncoder, withFormat format: LogEncoder.Format) {
18-
unimplemented()
18+
let ir = UIGraphicsImageRenderer(size: bounds.size)
19+
let pngData = ir.pngData { _ in
20+
self.drawHierarchy(in: bounds, afterScreenUpdates: true)
21+
}
22+
23+
encoder.encode(number: UInt64(pngData.count))
24+
encoder.encode(data: pngData)
1925
}
2026
}
2127
#endif

PlaygroundLogger/PlaygroundLoggerTests/LegacyPlaygroundLoggerTests.swift

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ import Foundation
2121

2222
#if os(macOS)
2323
import AppKit
24+
25+
typealias ImageType = NSImage
26+
#elseif os(iOS) || os(tvOS)
27+
import UIKit
28+
29+
typealias ImageType = UIImage
2430
#endif
2531

2632
import SpriteKit
@@ -243,8 +249,7 @@ class LegacyPlaygroundLoggerTests: XCTestCase {
243249
// testExceptionSafety() is excluded, as it tests functionality handled differently in the new implementation.
244250

245251
#if os(macOS)
246-
// This test is disabled pending support for logging images and views.
247-
func DISABLED_testNSViewLogging() {
252+
func testNSViewLogging() {
248253
let button = NSButton(frame: NSRect(x: 0,y: 0,width: 100,height: 100))
249254
let logdata = legacyLog(instance: button, name: "button", id: 0, startLine: 0, endLine: 0, startColumn: 0, endColumn: 0) as! NSData
250255
guard let decoded = legacyLogDecode(logdata) else {
@@ -256,14 +261,65 @@ class LegacyPlaygroundLoggerTests: XCTestCase {
256261
return
257262
}
258263
XCTAssertEqual(iderepr.tag, "VIEW")
264+
265+
guard let payloadImage = iderepr.payload as? NSImage else {
266+
XCTFail("Decoded payload is not an image")
267+
return
268+
}
269+
XCTAssertEqual(payloadImage.size, NSSize(width: 100, height: 100))
270+
259271
}
260-
261-
// This test is disabled pending support for logging images and views.
262-
func DISABLED_testNSImageLogging() {
263-
guard let image = NSImage(contentsOf: URL(string: "http://images.apple.com/support/assets/images/home/qp_apple_icon.png")!) else {
264-
XCTFail("Failed to load image")
272+
#endif
273+
274+
#if os(iOS) || os(tvOS)
275+
func testUIViewLogging() {
276+
let button = UIButton(type: .system)
277+
button.setTitle("Button", for: .normal)
278+
button.sizeToFit()
279+
280+
let logdata = legacyLog(instance: button, name: "button", id: 0, startLine: 0, endLine: 0, startColumn: 0, endColumn: 0) as! NSData
281+
guard let decoded = legacyLogDecode(logdata) else {
282+
XCTFail("Failed to decode log data")
283+
return
284+
}
285+
guard let iderepr = decoded.object as? PlaygroundDecodedObject_IDERepr else {
286+
XCTFail("Decoded object is not IDERepr")
287+
return
288+
}
289+
XCTAssertEqual(iderepr.tag, "VIEW")
290+
291+
guard let payloadImage = iderepr.payload as? UIImage else {
292+
XCTFail("Decoded payload is not an image")
265293
return
266294
}
295+
XCTAssertEqual(payloadImage.size, CGSize(width: button.bounds.size.width * UIScreen.main.scale, height: button.bounds.size.height * UIScreen.main.scale))
296+
}
297+
#endif
298+
299+
func testImageLogging() {
300+
let size = CGSize(width: 30, height: 30)
301+
302+
#if os(macOS)
303+
let image = NSImage(size: size, flipped: false) { rect -> Bool in
304+
NSColor.white.setFill()
305+
NSBezierPath(rect: rect).fill()
306+
NSColor.orange.setFill()
307+
NSBezierPath(roundedRect: rect.insetBy(dx: 5, dy: 5), xRadius: 3, yRadius: 3).fill()
308+
return true
309+
}
310+
#elseif os(iOS) || os(tvOS)
311+
let rendererFormat = UIGraphicsImageRendererFormat.preferred()
312+
rendererFormat.scale = 1
313+
rendererFormat.opaque = true
314+
315+
let image = UIGraphicsImageRenderer(size: size, format: rendererFormat).image { context in
316+
UIColor.white.setFill()
317+
UIBezierPath(rect: context.format.bounds).fill()
318+
UIColor.orange.setFill()
319+
UIBezierPath(roundedRect: context.format.bounds.insetBy(dx: 5, dy: 5), cornerRadius: 3).fill()
320+
}
321+
#endif
322+
267323
let logdata = legacyLog(instance: image, name: "image", id: 0, startLine: 0, endLine: 0, startColumn: 0, endColumn: 0) as! NSData
268324
guard let decoded = legacyLogDecode(logdata) else {
269325
XCTFail("Failed to decode log data")
@@ -274,10 +330,15 @@ class LegacyPlaygroundLoggerTests: XCTestCase {
274330
return
275331
}
276332
XCTAssertEqual(iderepr.tag, "IMAG")
333+
334+
guard let payloadImage = iderepr.payload as? ImageType else {
335+
XCTFail("Decoded payload is not an image")
336+
return
337+
}
338+
XCTAssertEqual(payloadImage.size, size)
277339
}
278340

279341
// testSpriteKitLogging() is excluded, as it cannot be trivially ported.
280-
#endif
281342

282343
func testOptionalGetsStripped() {
283344
let some: String?? = "hello"
@@ -403,9 +464,8 @@ class LegacyPlaygroundLoggerTests: XCTestCase {
403464
XCTAssertEqual(f, f2)
404465
XCTAssertEqual(d, d2)
405466
}
406-
407-
// This test is disabled pending support for logging images and views.
408-
func DISABLED_testSKShapeNode() {
467+
468+
func testSKShapeNode() {
409469
let blahNode = SKShapeNode(circleOfRadius: 30.0)
410470
let logdata = legacyLog(instance: blahNode, name: "blahNode", id: 0, startLine: 0, endLine: 0, startColumn: 0, endColumn: 0) as! NSData
411471
guard let decoded = legacyLogDecode(logdata) else {
@@ -418,6 +478,8 @@ class LegacyPlaygroundLoggerTests: XCTestCase {
418478
}
419479
XCTAssertEqual(bn_repr.tag, "SKIT")
420480
XCTAssertEqual(bn_repr.typeName, "SKShapeNode")
481+
482+
XCTAssert(bn_repr.payload is ImageType, "We expect the payload to be an image")
421483
}
422484

423485
func testBaseClassLogging() {
@@ -1379,12 +1441,11 @@ class PlaygroundIDEReprDecoder_URL: PlaygroundIDEReprDecoder {
13791441
}
13801442
}
13811443

1382-
#if os(macOS)
13831444
class PlaygroundIDEReprDecoder_Image: PlaygroundIDEReprDecoder {
13841445
class PlaygroundDecodedObject_IDERepr_Image: PlaygroundDecodedObject_IDERepr {
1385-
let data: NSImage
1446+
let data: ImageType
13861447

1387-
init (_ name: String, _ psum: Bool, _ brief: String, _ long: String, _ tag: String, _ data: NSImage) {
1448+
init (_ name: String, _ psum: Bool, _ brief: String, _ long: String, _ tag: String, _ data: ImageType) {
13881449
self.data = data
13891450
super.init(name, psum, brief, long, tag)
13901451
}
@@ -1399,11 +1460,10 @@ class PlaygroundIDEReprDecoder_Image: PlaygroundIDEReprDecoder {
13991460
}
14001461
func decodeObject(_ decoder: PlaygroundDecoder, _ bytes: BytesStorage, _ name: String, _ psum: Bool, _ brief: String, _ long: String, _ tag: String) -> PlaygroundDecodedObject_IDERepr? {
14011462
let data = bytes.data
1402-
guard let image = NSImage(data: data as Data) else { return nil }
1463+
guard let image = ImageType(data: data as Data) else { return nil }
14031464
return PlaygroundDecodedObject_IDERepr_Image(name, psum ,brief, long, tag, image)
14041465
}
14051466
}
1406-
#endif
14071467

14081468
class PlaygroundDecoder {
14091469
var bytes: BytesStorage
@@ -1440,9 +1500,9 @@ class PlaygroundDecoder {
14401500
self.iderepr_decoders["RANG"] = PlaygroundIDEReprDecoder_Range()
14411501
self.iderepr_decoders["BOOL"] = PlaygroundIDEReprDecoder_Bool()
14421502
self.iderepr_decoders["URL"] = PlaygroundIDEReprDecoder_URL()
1443-
#if os(macOS)
14441503
self.iderepr_decoders["IMAG"] = PlaygroundIDEReprDecoder_Image()
1445-
#endif
1504+
self.iderepr_decoders["VIEW"] = PlaygroundIDEReprDecoder_Image()
1505+
self.iderepr_decoders["SKIT"] = PlaygroundIDEReprDecoder_Image()
14461506
}
14471507

14481508
func getIDEReprDecoder(_ tag: String) -> PlaygroundIDEReprDecoder {

0 commit comments

Comments
 (0)