From 8e754c97e8e8e411e62d0b4fb632df11ffd78795 Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Mon, 24 Aug 2020 09:07:18 +0200 Subject: [PATCH] Fix msgtracer leak (#21) --- Sources/SwiftFrame/main.swift | 4 +- .../Config/ScreenshotData.swift | 2 +- .../Helper Types/GraphicsContext.swift | 37 +++++++++++++++++++ .../Workers/ConfigProcessor.swift | 4 +- .../Workers/DecodableParser.swift | 2 +- .../Workers/ImageComposer.swift | 27 +++----------- .../SwiftFrameCore/Workers/ImageWriter.swift | 4 +- .../Workers/ScreenshotRenderer.swift | 14 +++---- .../SwiftFrameCore/Workers/TextRenderer.swift | 8 ++-- .../SwiftFrameTests/ImageComposerTests.swift | 8 ++-- Tests/SwiftFrameTests/ImageLoaderTests.swift | 3 +- .../ScreenshotRendererTests.swift | 4 +- Tests/SwiftFrameTests/TextRendererTests.swift | 2 +- .../SwiftFrameTests/Utility/TestHelpers.swift | 20 ++-------- .../Utility/TestingUtility.swift | 4 +- 15 files changed, 76 insertions(+), 67 deletions(-) create mode 100644 Sources/SwiftFrameCore/Helper Types/GraphicsContext.swift diff --git a/Sources/SwiftFrame/main.swift b/Sources/SwiftFrame/main.swift index 66ef9b5..978a884 100644 --- a/Sources/SwiftFrame/main.swift +++ b/Sources/SwiftFrame/main.swift @@ -9,10 +9,10 @@ struct SwiftFrame: ParsableCommand { static let configuration = CommandConfiguration( commandName: "swiftframe", abstract: "CLI application for speedy screenshot framing", - version: "3.1.0", + version: "3.1.1", helpNames: .shortAndLong) - @Argument(help: "Read configuration values from the specified file", completion: .list(["config", "yml", "yaml"])) + @Argument(help: "Read configuration values from the specified file", completion: .list(["config", "json", "yml", "yaml"])) var configFilePath: String @Flag(name: .shortAndLong, help: "Prints additional information and lets you verify the config file before rendering") diff --git a/Sources/SwiftFrameCore/Config/ScreenshotData.swift b/Sources/SwiftFrameCore/Config/ScreenshotData.swift index 75d1c26..026ae86 100644 --- a/Sources/SwiftFrameCore/Config/ScreenshotData.swift +++ b/Sources/SwiftFrameCore/Config/ScreenshotData.swift @@ -26,7 +26,7 @@ public struct ScreenshotData: Decodable, ConfigValidatable, Equatable { // MARK: - Misc func makeProcessedData(size: CGSize) -> ScreenshotData { - return ScreenshotData( + ScreenshotData( screenshotName: screenshotName, bottomLeft: bottomLeft.convertingToBottomLeftOrigin(withSize: size), bottomRight: bottomRight.convertingToBottomLeftOrigin(withSize: size), diff --git a/Sources/SwiftFrameCore/Helper Types/GraphicsContext.swift b/Sources/SwiftFrameCore/Helper Types/GraphicsContext.swift new file mode 100644 index 0000000..717edc5 --- /dev/null +++ b/Sources/SwiftFrameCore/Helper Types/GraphicsContext.swift @@ -0,0 +1,37 @@ +import AppKit +import CoreGraphics +import Foundation + +class GraphicsContext { + + let cg: CGContext + private let colorSpace: CGColorSpace + + lazy var ci: CIContext = { + CIContext(cgContext: cg, options: [ + CIContextOption.workingColorSpace: colorSpace, + CIContextOption.useSoftwareRenderer: false + ]) + }() + + + init(size: CGSize) throws { + let colorSpace = CGColorSpaceCreateDeviceRGB() + let context = CGContext( + data: nil, + width: Int(size.width), + height: Int(size.height), + bitsPerComponent: 8, + bytesPerRow: 0, + space: colorSpace, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) + + guard let cgContext = context else { + throw NSError(description: "Failed to create graphics context") + } + self.cg = cgContext + self.colorSpace = colorSpace + } + +} diff --git a/Sources/SwiftFrameCore/Workers/ConfigProcessor.swift b/Sources/SwiftFrameCore/Workers/ConfigProcessor.swift index 06905c4..4fade3a 100644 --- a/Sources/SwiftFrameCore/Workers/ConfigProcessor.swift +++ b/Sources/SwiftFrameCore/Workers/ConfigProcessor.swift @@ -101,8 +101,6 @@ public class ConfigProcessor { color: data.textColorSource.color, maxFontSize: data.maxFontSize) - printVerbose("Writing images for device \"\(deviceData.outputSuffix)\" for locale \"\(locale)\" asynchronously...") - try ImageWriter.finish( context: composer.context, with: data.outputPaths, @@ -113,6 +111,8 @@ public class ConfigProcessor { suffix: deviceData.outputSuffix, format: data.outputFormat) + print("Finished \(locale)-\(deviceData.outputSuffix)") + group.leave() } diff --git a/Sources/SwiftFrameCore/Workers/DecodableParser.swift b/Sources/SwiftFrameCore/Workers/DecodableParser.swift index b6563d2..3cdfb6e 100644 --- a/Sources/SwiftFrameCore/Workers/DecodableParser.swift +++ b/Sources/SwiftFrameCore/Workers/DecodableParser.swift @@ -37,7 +37,7 @@ struct DecodableParser { init?(rawValue: String) { switch rawValue { - case "json": + case "config", "json": self = .json case "yml", "yaml": self = .yaml diff --git a/Sources/SwiftFrameCore/Workers/ImageComposer.swift b/Sources/SwiftFrameCore/Workers/ImageComposer.swift index 57e74a9..12ee441 100644 --- a/Sources/SwiftFrameCore/Workers/ImageComposer.swift +++ b/Sources/SwiftFrameCore/Workers/ImageComposer.swift @@ -17,29 +17,12 @@ final class ImageComposer { private let textRenderer = TextRenderer() private let screenshotRenderer = ScreenshotRenderer() - let context: CGContext + let context: GraphicsContext // MARK: - Init init(canvasSize: CGSize) throws { - self.context = try ImageComposer.createContext(size: canvasSize) - } - - // MARK: - Preparation - - private static func createContext(size: CGSize) throws -> CGContext { - guard let context = CGContext( - data: nil, - width: Int(size.width), - height: Int(size.height), - bitsPerComponent: 8, - bytesPerRow: 0, - space: CGColorSpaceCreateDeviceRGB(), - bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) - else { - throw NSError(description: "Failed to create graphics context") - } - return context + self.context = try GraphicsContext(size: canvasSize) } // MARK: - Composition @@ -49,9 +32,9 @@ final class ImageComposer { throw NSError(description: "Could not render template image") } - context.saveGState() - context.draw(templateImage, in: image.ky_nativeRect) - context.restoreGState() + context.cg.saveGState() + context.cg.draw(templateImage, in: image.ky_nativeRect) + context.cg.restoreGState() } // MARK: - Titles Rendering diff --git a/Sources/SwiftFrameCore/Workers/ImageWriter.swift b/Sources/SwiftFrameCore/Workers/ImageWriter.swift index 2161bf1..df4a510 100644 --- a/Sources/SwiftFrameCore/Workers/ImageWriter.swift +++ b/Sources/SwiftFrameCore/Workers/ImageWriter.swift @@ -6,7 +6,7 @@ public final class ImageWriter { // MARK: - Exporting static func finish( - context: CGContext, + context: GraphicsContext, with outputPaths: [FileURL], sliceSize: CGSize, gapWidth: Int, @@ -15,7 +15,7 @@ public final class ImageWriter { suffix: String, format: FileFormat) throws { - guard let image = context.makeImage() else { + guard let image = context.cg.makeImage() else { throw NSError(description: "Could not render output image") } let slices = try sliceImage(image, with: sliceSize, gapWidth: gapWidth) diff --git a/Sources/SwiftFrameCore/Workers/ScreenshotRenderer.swift b/Sources/SwiftFrameCore/Workers/ScreenshotRenderer.swift index 9fff42a..360fce3 100644 --- a/Sources/SwiftFrameCore/Workers/ScreenshotRenderer.swift +++ b/Sources/SwiftFrameCore/Workers/ScreenshotRenderer.swift @@ -5,16 +5,16 @@ final class ScreenshotRenderer { // MARK: - Screenshot Rendering - func render(screenshot: NSBitmapImageRep, with data: ScreenshotData, in context: CGContext) throws { - let cgImage = try renderScreenshot(screenshot, with: data) + func render(screenshot: NSBitmapImageRep, with data: ScreenshotData, in context: GraphicsContext) throws { + let cgImage = try renderScreenshot(screenshot, with: data, in: context) let rect = calculateRect(for: data) - context.saveGState() - context.draw(cgImage, in: rect) - context.restoreGState() + context.cg.saveGState() + context.cg.draw(cgImage, in: rect) + context.cg.restoreGState() } - private func renderScreenshot(_ screenshot: NSBitmapImageRep, with data: ScreenshotData) throws -> CGImage { + private func renderScreenshot(_ screenshot: NSBitmapImageRep, with data: ScreenshotData, in context: GraphicsContext) throws -> CGImage { let ciImage = CIImage(bitmapImageRep: screenshot) let perspectiveTransform = CIFilter(name: "CIPerspectiveTransform")! @@ -27,7 +27,7 @@ final class ScreenshotRenderer { guard let compositeImage = perspectiveTransform.outputImage, - let cgImage = CIContext().createCGImage(compositeImage, from: calculateRect(for: data)) + let cgImage = context.ci.createCGImage(compositeImage, from: calculateRect(for: data)) else { throw NSError(description: "Could not skew screenshot") } diff --git a/Sources/SwiftFrameCore/Workers/TextRenderer.swift b/Sources/SwiftFrameCore/Workers/TextRenderer.swift index 189236b..571bf73 100644 --- a/Sources/SwiftFrameCore/Workers/TextRenderer.swift +++ b/Sources/SwiftFrameCore/Workers/TextRenderer.swift @@ -8,7 +8,7 @@ final class TextRenderer { // MARK: - Frame Rendering - func render(text: String, font: NSFont, color: NSColor, alignment: TextAlignment, rect: NSRect, context: CGContext) throws { + func render(text: String, font: NSFont, color: NSColor, alignment: TextAlignment, rect: NSRect, context: GraphicsContext) throws { guard !text.isEmpty else { print(CommandLineFormatter.formatWarning(text: "String was emtpy and will not be rendered")) return @@ -16,11 +16,11 @@ final class TextRenderer { let attributedString = try makeAttributedString(for: text, font: font, color: color, alignment: alignment) - context.saveGState() + context.cg.saveGState() let frame = try makeFrame(from: attributedString, in: rect, alignment: alignment) - CTFrameDraw(frame, context) - context.restoreGState() + CTFrameDraw(frame, context.cg) + context.cg.restoreGState() } private func makeFrame(from attributedString: NSAttributedString, in rect: NSRect, alignment: TextAlignment) throws -> CTFrame { diff --git a/Tests/SwiftFrameTests/ImageComposerTests.swift b/Tests/SwiftFrameTests/ImageComposerTests.swift index d19e264..538fa80 100644 --- a/Tests/SwiftFrameTests/ImageComposerTests.swift +++ b/Tests/SwiftFrameTests/ImageComposerTests.swift @@ -6,22 +6,22 @@ class ImageComposerTests: XCTestCase { func testRenderTemplateFile() throws { let size = CGSize(width: 100, height: 50) - let templateFile = CGContext.makeImageRepWithSize(size) + let templateFile = try GraphicsContext(size: size).cg.makePlainWhiteImageRep() let composer = try ImageComposer(canvasSize: size) try composer.addTemplateImage(templateFile) - let image = try ky_unwrap(composer.context.makeImage()) + let image = try ky_unwrap(composer.context.cg.makeImage()) XCTAssertEqual(image.width, Int(size.width)) XCTAssertEqual(image.height, Int(size.height)) } func testTemplateImageSlicesCorrectly() throws { let size = CGSize(width: 100, height: 50) - let templateFile = CGContext.makeImageRepWithSize(size) + let templateFile = try GraphicsContext(size: size).cg.makePlainWhiteImageRep() let composer = try ImageComposer(canvasSize: size) try composer.addTemplateImage(templateFile) - let image = try ky_unwrap(composer.context.makeImage()) + let image = try ky_unwrap(composer.context.cg.makeImage()) let slices = try ImageWriter.sliceImage(image, with: NSSize(width: 20, height: 50), gapWidth: 0) XCTAssertEqual(slices.count, 5) for slice in slices { diff --git a/Tests/SwiftFrameTests/ImageLoaderTests.swift b/Tests/SwiftFrameTests/ImageLoaderTests.swift index 113dbdf..bd3ba6f 100644 --- a/Tests/SwiftFrameTests/ImageLoaderTests.swift +++ b/Tests/SwiftFrameTests/ImageLoaderTests.swift @@ -5,7 +5,8 @@ import XCTest class ImageLoaderTests: BaseTest { func testLoadImage() throws { - let rep = CGContext.makeImageRepWithSize(.square100Pixels) + let context = try GraphicsContext(size: .square100Pixels) + let rep = context.cg.makePlainWhiteImageRep() let cgImage = try ky_unwrap(rep.cgImage) try ImageWriter.write(cgImage, to: "testing/", locale: "en", deviceID: "testing_device", format: .png) diff --git a/Tests/SwiftFrameTests/ScreenshotRendererTests.swift b/Tests/SwiftFrameTests/ScreenshotRendererTests.swift index b51ae16..18136f4 100644 --- a/Tests/SwiftFrameTests/ScreenshotRendererTests.swift +++ b/Tests/SwiftFrameTests/ScreenshotRendererTests.swift @@ -6,8 +6,8 @@ class ScreenshotRendererTests: XCTestCase { func testRenderScreenshot() throws { let size = CGSize(width: 100, height: 100) - let context = CGContext.with(size: size) - let rep = CGContext.makeImageRepWithSize(size) + let context = try GraphicsContext(size: size) + let rep = context.cg.makePlainWhiteImageRep() let mockScreenshotData = ScreenshotData.goodData diff --git a/Tests/SwiftFrameTests/TextRendererTests.swift b/Tests/SwiftFrameTests/TextRendererTests.swift index 616dc2b..07a2e31 100644 --- a/Tests/SwiftFrameTests/TextRendererTests.swift +++ b/Tests/SwiftFrameTests/TextRendererTests.swift @@ -35,7 +35,7 @@ class TextRendererTests: XCTestCase { let size = CGSize(width: 100, height: 100) let rect = NSRect(x: 10, y: 10, width: 80, height: 80) - let context = CGContext.with(size: size) + let context = try GraphicsContext(size: size) try renderer.render( text: "Some title", font: .systemFont(ofSize: 20), diff --git a/Tests/SwiftFrameTests/Utility/TestHelpers.swift b/Tests/SwiftFrameTests/Utility/TestHelpers.swift index 39c4605..2c0eb29 100644 --- a/Tests/SwiftFrameTests/Utility/TestHelpers.swift +++ b/Tests/SwiftFrameTests/Utility/TestHelpers.swift @@ -33,22 +33,10 @@ extension Dictionary where Value == String, Key == String { extension CGContext { - static func with(size: CGSize) -> CGContext { - CGContext( - data: nil, - width: Int(size.width), - height: Int(size.height), - bitsPerComponent: 8, - bytesPerRow: 0, - space: CGColorSpaceCreateDeviceRGB(), - bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)! - } - - static func makeImageRepWithSize(_ size: CGSize) -> NSBitmapImageRep { - let context = CGContext.with(size: size) - context.setFillColor(.white) - context.fill(NSRect(x: 0, y: 0, width: size.width, height: size.height)) - return NSBitmapImageRep(cgImage: context.makeImage()!) + func makePlainWhiteImageRep() -> NSBitmapImageRep { + setFillColor(.white) + fill(NSRect(x: 0, y: 0, width: width, height: height)) + return NSBitmapImageRep(cgImage: makeImage()!) } } diff --git a/Tests/SwiftFrameTests/Utility/TestingUtility.swift b/Tests/SwiftFrameTests/Utility/TestingUtility.swift index b9585b6..31ab290 100644 --- a/Tests/SwiftFrameTests/Utility/TestingUtility.swift +++ b/Tests/SwiftFrameTests/Utility/TestingUtility.swift @@ -6,7 +6,7 @@ public typealias JSONDictionary = [String: Encodable] struct TestingUtility { static func writeMockScreenshot(locale: String, deviceSuffix: String) throws { - let rep = CGContext.makeImageRepWithSize(.square100Pixels) + let rep = try GraphicsContext(size: .square100Pixels).cg.makePlainWhiteImageRep() guard let cgImage = rep.cgImage else { throw NSError(description: "Could not make CGImage from Bitmap") } @@ -16,7 +16,7 @@ struct TestingUtility { } static func writeMockTemplateFile(deviceSuffix: String, gapWidth: Int) throws { - let rep = CGContext.makeImageRepWithSize(.make100PixelsSize(with: gapWidth, numberOfGaps: 4)) + let rep = try GraphicsContext(size: .make100PixelsSize(with: gapWidth, numberOfGaps: 4)).cg.makePlainWhiteImageRep() guard let cgImage = rep.cgImage else { throw NSError(description: "Could not make CGImage from Bitmap") }