Repository: nst/BitmapCanvas Branch: master Commit: 1cf51fb6794d Files: 13 Total size: 2.9 MB Directory structure: gitextract_8mknh6zh/ ├── .gitignore ├── BitmapCanvas/ │ └── main.swift ├── BitmapCanvas.swift ├── BitmapCanvas.xcodeproj/ │ ├── project.pbxproj │ └── project.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── README.md ├── X11Colors.swift └── files/ ├── e_2000000.txt ├── pi_1000000.txt ├── results.json └── switzerland.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## Build generated build/ DerivedData ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata ## Other *.xccheckout *.moved-aside *.xcuserstate *.xcscmblueprint ## Obj-C/Swift specific *.hmap *.ipa ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ .build/ # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # Pods/ # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build # fastlane # # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # screenshots whenever they are needed. # For more information about the recommended setup visit: # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md fastlane/report.xml fastlane/screenshots ================================================ FILE: BitmapCanvas/main.swift ================================================ // // main.swift // BitmapCanvas // // Created by nst on 28/02/16. // Copyright © 2016 Nicolas Seriot. All rights reserved. // import Foundation import AppKit let PROJECT_PATH = "/Users/nst/Projects/BitmapCanvas" func switzerland() { guard let resultsData = try? Data(contentsOf: URL(fileURLWithPath: PROJECT_PATH+"/files/results.json")) else { return } guard let optResults = try? JSONSerialization.jsonObject(with: resultsData, options: []) as? [String:AnyObject] else { return } guard let results = optResults else { return } guard let switzerlandData = try? Data(contentsOf: URL(fileURLWithPath: PROJECT_PATH + "/files/switzerland.json")) else { return } guard let optSwitzerland = try? JSONSerialization.jsonObject(with: switzerlandData, options: []) as? [String:AnyObject] else { return } guard let switzerland = optSwitzerland else { return } let b = BitmapCanvas(365, 235, "white") b.image(fromPath: PROJECT_PATH+"/files/switzerland.gif", P(0,0)) b.text("2016-02-28 \"Pas de spéculation sur les denrées alimentaires\"", P(5,220)) let values : [Double] = results.compactMap { (k,v) in v as? Double } let positiveValues = values.filter { $0 >= 50.0 } let negativeValues = values.filter { $0 < 50.0 } let minPositive = positiveValues.min() ?? 0.0 let maxPositive = positiveValues.max() ?? 0.01 let minNegative = negativeValues.min() ?? 0.0 let maxNegative = negativeValues.max() ?? 0.01 let positiveRange = maxPositive - minPositive let negativeRange = maxNegative - minNegative print(minPositive, maxPositive) print(minNegative, maxNegative) for (k, cantonDict) in switzerland { guard let labelPoint = cantonDict["label"] as? [Int] else { continue } // fill color var fillColor: NSColor = .lightGray if let percent = results[k] as? Double { if percent < 50.0 { let i: Double = ((percent - minNegative) / negativeRange) - 0.15 fillColor = NSColor(calibratedRed: 1.0, green: CGFloat(i), blue: i.cgFloat, alpha: 1.0) } else { let i: Double = (1.0 - (percent - minPositive) / positiveRange) - 0.15 fillColor = NSColor(calibratedRed: i.cgFloat, green: 1.0, blue: CGFloat(i), alpha: 1.0) } } // fill cantons let fillPoints = cantonDict["fill"] as? [[Int]] ?? [labelPoint] for pts in fillPoints { let p = P(pts[0], pts[1]) b.fill(p, color: fillColor) } // draw labels let p = P(labelPoint[0], labelPoint[1]) b.text(k, p) } let path = "/tmp/out.png" b.save(path, open:true) } func bitmap() { let b = BitmapCanvas(32, 32, "PapayaWhip") b.save("/tmp/bitmap.png") } func points() { let b = BitmapCanvas(32, 32, "PapayaWhip") b[1,1] = NSColor.black b[1,3] = "red" b[2,3] = "#00FF00" b[3,3] = NSColor.blue b.save("/tmp/points.png") } func lines() { let b = BitmapCanvas(32, 32, "PapayaWhip") b.line(P(1,1), P(10,10)) b.line(P(1,10), P(10,19), "red") b.lineHorizontal(P(1,21), width:20) b.lineVertical(P(20, 1), height:19, "blue") b.save("/tmp/lines.png") } func rects() { let b = BitmapCanvas(32, 32, "PapayaWhip") b.rectangle(R(5,5,20,10)) b.rectangle(R(10,10,20,10), stroke:"blue", fill:"magenta") b.save("/tmp/rects.png") } func ellipse() { let b = BitmapCanvas(32, 32, "PapayaWhip") b.ellipse(R(5,5,20,10)) b.ellipse(R(10,10,18,21), stroke:"blue", fill:"magenta") b.save("/tmp/ellipse.png", open:true) } func fill() { let b = BitmapCanvas(32, 32, "PapayaWhip") b.line(P(10,0), P(25,31), "red") b.ellipse(R(15,10,15,15)) b.fill(P(30,1), color:"yellow") b.save("/tmp/fill.png") } func text() { let b = BitmapCanvas(32, 32, "PapayaWhip") b.text("hi", P(5,10)) b.text("hello", P(20,30), rotationDegrees: -90, font: NSFont(name: "Helvetica", size: 10)!, color: NSColor.red) b.save("/tmp/text.png") } func image() { let b = BitmapCanvas(32, 32, "PapayaWhip") b.image(fromPath:"/usr/share/httpd/icons/sphere2.png", P(0,0)) b.save("/tmp/image.png", open:true) } func polygon() { let b = BitmapCanvas(32, 32, "PapayaWhip") b.setAllowsAntialiasing(true) let points = [P(3,3), P(28,5), P(25,22), P(12,18)] b.polygon(points, stroke:"blue", fill:"SkyBlue") b.save("/tmp/polygon.png", open:true) } func bezier() { let b = BitmapCanvas(32, 32, "PapayaWhip") b.setAllowsAntialiasing(true) NSColor.orange.setFill() let bp = NSBezierPath() bp.move(to: P(2,2)) bp.curve(to: P(20,14), controlPoint1: P(14,30), controlPoint2: P(15,30)) bp.curve(to: P(32,13), controlPoint1: P(24,14), controlPoint2: P(24,19)) bp.close() bp.fill() bp.stroke() b.save("/tmp/bezier.png") } func cgContext() { let b = BitmapCanvas(32, 32, "PapayaWhip") let c = b.cgContext c.addEllipse(in: R(2, 2, 24, 24)) c.strokePath() b.setAllowsAntialiasing(true) c.setStrokeColor(NSColor.blue.cgColor) c.addEllipse(in: R(12, 12, 24, 24)) c.strokePath() b.save("/tmp/cgcontext.png") } func gradient() { let (w, h) = (255, 255) let b = BitmapCanvas(w, h) for i in 0..(_ x:T, _ y:T) -> NSPoint { return NSMakePoint(x.cgFloat, y.cgFloat) } infix operator * : MultiplicationPrecedence func *(left:GenericNumber, right:GenericNumber) -> Double { return left.double * right.double } infix operator / : MultiplicationPrecedence func /(left:GenericNumber, right:GenericNumber) -> Double { return left.double / right.double } infix operator + : AdditionPrecedence func +(left:GenericNumber, right:GenericNumber) -> Double { return left.double + right.double } infix operator - : AdditionPrecedence func -(left:GenericNumber, right:GenericNumber) -> Double { return left.double - right.double } func RandomPoint(maxX:Int, maxY:Int) -> NSPoint { return P(arc4random_uniform(UInt32(maxX+1)), arc4random_uniform(UInt32(maxY+1))) } func R(_ x:T, _ y:T, _ w:T, _ h:T) -> NSRect { return NSMakeRect(x.cgFloat, y.cgFloat, w.cgFloat, h.cgFloat) } class BitmapCanvas { let bitmapImageRep : NSBitmapImageRep let context : NSGraphicsContext var cgContext : CGContext { return context.cgContext } var width : CGFloat { return bitmapImageRep.size.width } var height : CGFloat { return bitmapImageRep.size.height } func setAllowsAntialiasing(_ antialiasing : Bool) { cgContext.setAllowsAntialiasing(antialiasing) } init(_ width:Int, _ height:Int, _ background:ConvertibleToNSColor? = nil) { self.bitmapImageRep = NSBitmapImageRep( bitmapDataPlanes:nil, pixelsWide:width, pixelsHigh:height, bitsPerSample:8, samplesPerPixel:4, hasAlpha:true, isPlanar:false, colorSpaceName:NSColorSpaceName.deviceRGB, bytesPerRow:width*4, bitsPerPixel:32)! self.context = NSGraphicsContext(bitmapImageRep: bitmapImageRep)! NSGraphicsContext.current = context setAllowsAntialiasing(false) if let b = background { let rect = NSMakeRect(0, 0, CGFloat(width), CGFloat(height)) context.saveGraphicsState() b.color.setFill() NSBezierPath.fill(rect) context.restoreGraphicsState() } // makes coordinates start upper left cgContext.translateBy(x: 0, y: CGFloat(height)) cgContext.scaleBy(x: 1.0, y: -1.0) } fileprivate func _colorIsEqual(_ p:NSPoint, _ pixelBuffer:UnsafePointer, _ rgba:(UInt8,UInt8,UInt8,UInt8)) -> Bool { let offset = 4 * ((Int(self.width) * Int(p.y) + Int(p.x))) let r = pixelBuffer[offset] let g = pixelBuffer[offset+1] let b = pixelBuffer[offset+2] let a = pixelBuffer[offset+3] if r != rgba.0 { return false } if g != rgba.1 { return false } if b != rgba.2 { return false } if a != rgba.3 { return false } return true } fileprivate func _color(_ p:NSPoint, pixelBuffer:UnsafePointer) -> NSColor { let offset = 4 * ((Int(self.width) * Int(p.y) + Int(p.x))) let r = pixelBuffer[offset] let g = pixelBuffer[offset+1] let b = pixelBuffer[offset+2] let a = pixelBuffer[offset+3] return NSColor( calibratedRed: CGFloat(Double(r)/255.0), green: CGFloat(Double(g)/255.0), blue: CGFloat(Double(b)/255.0), alpha: CGFloat(Double(a)/255.0)) } func color(_ p:NSPoint) -> NSColor { guard let data = cgContext.data else { assertionFailure(); return NSColor.clear } let pixelBuffer = data.assumingMemoryBound(to: UInt8.self) return _color(p, pixelBuffer:pixelBuffer) } fileprivate func _setColor(_ p:NSPoint, pixelBuffer:UnsafeMutablePointer, normalizedColor:NSColor) { let offset = 4 * ((Int(self.width) * Int(p.y) + Int(p.x))) pixelBuffer[offset] = UInt8(normalizedColor.redComponent * 255.0) pixelBuffer[offset+1] = UInt8(normalizedColor.greenComponent * 255.0) pixelBuffer[offset+2] = UInt8(normalizedColor.blueComponent * 255.0) pixelBuffer[offset+3] = UInt8(normalizedColor.alphaComponent * 255.0) } func setColor(_ p:NSPoint, color color_:ConvertibleToNSColor) { let color = color_.color guard let normalizedColor = color.usingColorSpaceName(NSColorSpaceName.calibratedRGB) else { print("-- cannot normalize color \(color)") return } guard let data = cgContext.data else { assertionFailure(); return } let pixelBuffer = data.assumingMemoryBound(to: UInt8.self) _setColor(p, pixelBuffer:pixelBuffer, normalizedColor:normalizedColor) } subscript(x:Int, y:Int) -> ConvertibleToNSColor { get { let p = P(x, y) return color(p) } set { let p = P(x, y) setColor(p, color:newValue) } } func fill(_ p:NSPoint, color rawNewColor_:ConvertibleToNSColor) { // floodFillScanlineStack from http://lodev.org/cgtutor/floodfill.html let rawNewColor = rawNewColor_.color assert(p.x < width, "p.x \(p.x) out of range, must be < \(width)") assert(p.y < height, "p.y \(p.y) out of range, must be < \(height)") guard let data = cgContext.data else { assertionFailure(); return } let pixelBuffer = data.assumingMemoryBound(to: UInt8.self) guard let newColor = rawNewColor.usingColorSpaceName(NSColorSpaceName.calibratedRGB) else { print("-- cannot normalize color \(rawNewColor)") return } let oldColor = _color(p, pixelBuffer:pixelBuffer) if oldColor == newColor { return } // store rgba as [UInt8] to speed up comparisons var r : CGFloat = 0.0 var g : CGFloat = 0.0 var b : CGFloat = 0.0 var a : CGFloat = 0.0 oldColor.getRed(&r, green: &g, blue: &b, alpha: &a) let rgba = (UInt8(r*255),UInt8(g*255),UInt8(b*255),UInt8(a*255)) var stack : [NSPoint] = [p] while let pp = stack.popLast() { var x1 = pp.x while(x1 >= 0 && _color(P(x1, pp.y), pixelBuffer:pixelBuffer) == oldColor) { x1 -= 1 } x1 += 1 var spanAbove = false var spanBelow = false while(x1 < width && _colorIsEqual(P(x1, pp.y), pixelBuffer, rgba )) { _setColor(P(x1, pp.y), pixelBuffer:pixelBuffer, normalizedColor:newColor) let north = P(x1, pp.y-1) let south = P(x1, pp.y+1) if spanAbove == false && pp.y > 0 && _colorIsEqual(north, pixelBuffer, rgba) { stack.append(north) spanAbove = true } else if spanAbove && pp.y > 0 && !_colorIsEqual(north, pixelBuffer, rgba) { spanAbove = false } else if spanBelow == false && pp.y < height - 1 && _colorIsEqual(south, pixelBuffer, rgba) { stack.append(south) spanBelow = true } else if spanBelow && pp.y < height - 1 && !_colorIsEqual(south, pixelBuffer, rgba) { spanBelow = false } x1 += 1 } } } func line(_ p1: NSPoint, _ p2: NSPoint, _ color_: ConvertibleToNSColor? = NSColor.black) { let color = color_?.color context.saveGraphicsState() // align to the pixel grid cgContext.translateBy(x: 0.5, y: 0.5) if let existingColor = color { cgContext.setStrokeColor(existingColor.cgColor); } cgContext.setLineCap(.square) cgContext.move(to: CGPoint(x: p1.x, y: p1.y)) cgContext.addLine(to: CGPoint(x: p2.x, y: p2.y)) cgContext.strokePath() context.restoreGraphicsState() } func line(_ p1: NSPoint, length: GenericNumber = 1.0, degreesCW: GenericNumber = 0.0, _ color_: ConvertibleToNSColor? = NSColor.black) -> NSPoint { let color = color_?.color let radians = degreesToRadians(degreesCW.double) let p2 = P(p1.x + sin(radians) * length.cgFloat, p1.y - cos(radians) * length.cgFloat) self.line(p1, p2, color) return p2 } func lineVertical(_ p1: NSPoint, height: Double, _ color_: ConvertibleToNSColor? = nil) { let color = color_?.color let p2 = P(p1.x, p1.y + CGFloat(height - 1)) self.line(p1, p2, color) } func lineHorizontal(_ p1:NSPoint, width: Double, _ color_: ConvertibleToNSColor? = nil) { let color = color_?.color let p2 = P(p1.x + width - 1, p1.y.double) self.line(p1, p2, color) } func line(_ p1: NSPoint, deltaX: Double, deltaY: Double, _ color_: ConvertibleToNSColor? = nil) { let color = color_?.color let p2 = P(p1.x + deltaX, p1.y + deltaY) self.line(p1, p2, color) } func rectangle(_ rect:NSRect, stroke stroke_:ConvertibleToNSColor? = NSColor.black, fill fill_:ConvertibleToNSColor? = nil) { let stroke = stroke_?.color let fill = fill_?.color context.saveGraphicsState() // align to the pixel grid cgContext.translateBy(x: 0.5, y: 0.5) if let existingFillColor = fill { existingFillColor.setFill() NSBezierPath.fill(rect) } if let existingStrokeColor = stroke { existingStrokeColor.setStroke() NSBezierPath.stroke(rect) } context.restoreGraphicsState() } func polygon(_ points:[NSPoint], stroke stroke_:ConvertibleToNSColor? = NSColor.black, lineWidth:CGFloat=1.0, fill fill_:ConvertibleToNSColor? = nil) { guard points.count >= 3 else { assertionFailure("at least 3 points are needed") return } context.saveGraphicsState() let path = NSBezierPath() path.move(to: points[0]) for i in 1...points.count-1 { path.line(to: points[i]) } if let existingFillColor = fill_?.color { existingFillColor.setFill() path.fill() } path.close() if let existingStrokeColor = stroke_?.color { existingStrokeColor.setStroke() path.lineWidth = lineWidth path.stroke() } context.restoreGraphicsState() } func ellipse(_ rect:NSRect, stroke stroke_:ConvertibleToNSColor? = NSColor.black, fill fill_:ConvertibleToNSColor? = nil) { let strokeColor = stroke_?.color let fillColor = fill_?.color context.saveGraphicsState() // align to the pixel grid cgContext.translateBy(x: 0.5, y: 0.5) // fill if let existingFillColor = fillColor { existingFillColor.setFill() // reduce fillRect so that is doesn't cross the stoke let fillRect = R(rect.origin.x+1, rect.origin.y+1, rect.size.width-2, rect.size.height-2) cgContext.fillEllipse(in: fillRect) } // stroke if let existingStrokeColor = strokeColor { existingStrokeColor.setStroke() } cgContext.strokeEllipse(in: rect) context.restoreGraphicsState() } fileprivate func degreesToRadians(_ x:GenericNumber) -> Double { return (Double.pi * x / 180.0) } func rotate(center p: CGPoint, radians: GenericNumber, closure: () -> ()) { let c = self.cgContext c.saveGState() c.translateBy(x: p.x, y: p.y) c.rotate(by: radians.cgFloat) c.translateBy(x: p.x * -1.0, y: p.y * -1.0) closure() c.restoreGState() } func save(_ path:String, open:Bool=false) { guard let data = bitmapImageRep.representation(using: .png, properties: [:]) else { print("\(#file) \(#function) cannot get PNG data from bitmap") return } do { try data.write(to: URL(fileURLWithPath: path), options: []) if open { NSWorkspace.shared.openFile(path) } } catch let e { print(e) } } static func textWidth(_ text:NSString, font:NSFont) -> CGFloat { let maxSize : CGSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: font.pointSize) let textRect : CGRect = text.boundingRect( with: maxSize, options: NSString.DrawingOptions.usesLineFragmentOrigin, attributes: [.font: font], context: nil) return textRect.size.width } func image(fromPath path:String, _ p:NSPoint) { guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { print("\(#file) \(#function) cannot read data at \(path)"); return } guard let imgRep = NSBitmapImageRep(data: data) else { print("\(#file) \(#function) cannot create bitmap image rep from data at \(path)"); return } guard let cgImage = imgRep.cgImage else { print("\(#file) \(#function) cannot get cgImage out of imageRep from \(path)"); return } context.saveGraphicsState() cgContext.scaleBy(x: 1.0, y: -1.0) cgContext.translateBy(x: 0.0, y: -2.0 * p.y - imgRep.pixelsHigh.cgFloat) let rect = NSMakeRect(p.x, p.y, imgRep.pixelsWide.cgFloat, imgRep.pixelsHigh.cgFloat) cgContext.draw(cgImage, in: rect) context.restoreGraphicsState() } func text(_ text: String, _ p: NSPoint, rotationRadians: GenericNumber?, font: NSFont = NSFont(name: "Monaco", size: 10)!, color color_ : ConvertibleToNSColor = NSColor.black) { let color = color_.color context.saveGraphicsState() if let radians = rotationRadians { cgContext.translateBy(x: p.x, y: p.y); cgContext.rotate(by: radians.cgFloat) cgContext.translateBy(x: -p.x, y: -p.y); } cgContext.scaleBy(x: 1.0, y: -1.0) cgContext.translateBy(x: 0.0, y: -2.0 * p.y - font.pointSize) text.draw(at: p, withAttributes: [.font: font, .foregroundColor: color]) context.restoreGraphicsState() } func text(_ text: String, _ p: NSPoint, rotationDegrees degrees: GenericNumber = 0.0, font: NSFont = NSFont(name: "Monaco", size: 10)!, color: ConvertibleToNSColor = NSColor.black) { self.text(text, p, rotationRadians: degreesToRadians(degrees), font: font, color: color) } } ================================================ FILE: BitmapCanvas.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 03E114E31C832C83000B8942 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E114E21C832C83000B8942 /* main.swift */; }; 03E114EA1C832C8B000B8942 /* BitmapCanvas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E114E91C832C8B000B8942 /* BitmapCanvas.swift */; }; 03E834F91C8C6AB300FAC6AD /* X11Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E834F81C8C6AB300FAC6AD /* X11Colors.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 03E114DD1C832C83000B8942 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = /usr/share/man/man1/; dstSubfolderSpec = 0; files = ( ); runOnlyForDeploymentPostprocessing = 1; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 03E114DF1C832C83000B8942 /* BitmapCanvas */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = BitmapCanvas; sourceTree = BUILT_PRODUCTS_DIR; }; 03E114E21C832C83000B8942 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 03E114E91C832C8B000B8942 /* BitmapCanvas.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitmapCanvas.swift; sourceTree = ""; }; 03E114EB1C832F1A000B8942 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 03E834F81C8C6AB300FAC6AD /* X11Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = X11Colors.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 03E114DC1C832C83000B8942 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 03E114D61C832C83000B8942 = { isa = PBXGroup; children = ( 03E114EB1C832F1A000B8942 /* README.md */, 03E834F81C8C6AB300FAC6AD /* X11Colors.swift */, 03E114E91C832C8B000B8942 /* BitmapCanvas.swift */, 03E114E11C832C83000B8942 /* BitmapCanvas */, 03E114E01C832C83000B8942 /* Products */, ); sourceTree = ""; }; 03E114E01C832C83000B8942 /* Products */ = { isa = PBXGroup; children = ( 03E114DF1C832C83000B8942 /* BitmapCanvas */, ); name = Products; sourceTree = ""; }; 03E114E11C832C83000B8942 /* BitmapCanvas */ = { isa = PBXGroup; children = ( 03E114E21C832C83000B8942 /* main.swift */, ); path = BitmapCanvas; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 03E114DE1C832C83000B8942 /* BitmapCanvas */ = { isa = PBXNativeTarget; buildConfigurationList = 03E114E61C832C83000B8942 /* Build configuration list for PBXNativeTarget "BitmapCanvas" */; buildPhases = ( 03E114DB1C832C83000B8942 /* Sources */, 03E114DC1C832C83000B8942 /* Frameworks */, 03E114DD1C832C83000B8942 /* CopyFiles */, ); buildRules = ( ); dependencies = ( ); name = BitmapCanvas; productName = BitmapCanvas; productReference = 03E114DF1C832C83000B8942 /* BitmapCanvas */; productType = "com.apple.product-type.tool"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 03E114D71C832C83000B8942 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0720; LastUpgradeCheck = 0720; ORGANIZATIONNAME = "Nicolas Seriot"; TargetAttributes = { 03E114DE1C832C83000B8942 = { CreatedOnToolsVersion = 7.2.1; LastSwiftMigration = 1010; }; }; }; buildConfigurationList = 03E114DA1C832C83000B8942 /* Build configuration list for PBXProject "BitmapCanvas" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( en, ); mainGroup = 03E114D61C832C83000B8942; productRefGroup = 03E114E01C832C83000B8942 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 03E114DE1C832C83000B8942 /* BitmapCanvas */, ); }; /* End PBXProject section */ /* Begin PBXSourcesBuildPhase section */ 03E114DB1C832C83000B8942 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 03E114EA1C832C8B000B8942 /* BitmapCanvas.swift in Sources */, 03E834F91C8C6AB300FAC6AD /* X11Colors.swift in Sources */, 03E114E31C832C83000B8942 /* main.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ 03E114E41C832C83000B8942 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 03E114E51C832C83000B8942 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; }; name = Release; }; 03E114E71C832C83000B8942 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.2; }; name = Debug; }; 03E114E81C832C83000B8942 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.2; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 03E114DA1C832C83000B8942 /* Build configuration list for PBXProject "BitmapCanvas" */ = { isa = XCConfigurationList; buildConfigurations = ( 03E114E41C832C83000B8942 /* Debug */, 03E114E51C832C83000B8942 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 03E114E61C832C83000B8942 /* Build configuration list for PBXNativeTarget "BitmapCanvas" */ = { isa = XCConfigurationList; buildConfigurations = ( 03E114E71C832C83000B8942 /* Debug */, 03E114E81C832C83000B8942 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 03E114D71C832C83000B8942 /* Project object */; } ================================================ FILE: BitmapCanvas.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: BitmapCanvas.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Nicolas Seriot Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # BitmapCanvas __Bitmap offscreen drawing in Swift for OS X__ __Description__ A clear, simple and concise API over a CoreGraphics bitmap context. Loosely inspired by the ImageDraw Python library. Especially useful for easy [data](https://github.com/nst/DevTeamActivity) [visualizations](https://github.com/nst/ShapefileReader). __Features__ * upper-left corner based coordinates * pixel perfect drawing of points, lines, rectangles, ellipses and texts * colors as NSColor, hex strings ``"#AA00DF"`` or X11 names ``"SkyBlue"`` * save as PNG * usable with regular CoreGraphics code __Examples__
let b = BitmapCanvas(32, 32, "PapayaWhip")
b[1,1] = NSColor.black

b[1,3] = "red"
b[2,3] = "#00FF00"
b[3,3] = NSColor.blue
b.line(P(1,1), P(10,10))

b.line(P(1,10), P(10,19), "red")
b.lineHorizontal(P(1,21), width:20)
b.lineVertical(P(20, 1), height:19, "blue")
b.rectangle(R(5,5,20,10))

b.rectangle(R(10,10,20,10), stroke:"blue", fill:"magenta")
b.ellipse(R(5,5,20,10))

b.ellipse(R(10,10,18,21), stroke:"blue", fill:"magenta")
let b = BitmapCanvas(32, 32, "PapayaWhip")

b.line(P(10,0), P(25,31), "red")

b.ellipse(R(15,10,15,15))

b.fill(P(30,1), color:"yellow")
b.text("hi", P(5,10))

b.text("hello", P(20,30),
    rotationDegrees: -90,
    font: NSFont(name: "Helvetica", size: 10)!,
    color: NSColor.red)
b.image(fromPath:"/usr/share/httpd/icons/sphere2.png", P(0,0))
let b = BitmapCanvas(32, 32, "PapayaWhip")

b.setAllowsAntialiasing(true)

let points = [P(3,3), P(28,5), P(25,22), P(12,18)]

b.polygon(points, stroke:"blue", fill:"SkyBlue")

b.save("/tmp/polygon.png", open:true)
b.setAllowsAntialiasing(true)

NSColor.orangeColor().setFill()

let bp = NSBezierPath()
bp.moveToPoint(P(2,2))
bp.curveToPoint(P(20,14), controlPoint1: P(14,30), controlPoint2: P(15,30))
bp.curveToPoint(P(32,13), controlPoint1: P(24,14), controlPoint2: P(24,19))
bp.closePath()
bp.fill()
bp.stroke()
let b = BitmapCanvas(32, 32, "PapayaWhip")
let c = b.cgContext

c.addEllipse(in: R(2, 2, 24, 24))
c.strokePath()

b.setAllowsAntialiasing(true)

c.setStrokeColor(NSColor.blue.cgColor)
c.addEllipse(in: R(12, 12, 24, 24))
c.strokePath()

b.save("/tmp/cgcontext.png")
b.save("/tmp/image.png", open:true)
You can also dump the X11 color list with: X11Colors.dump("/opt/X11/share/X11/rgb.txt", outPath:"/tmp/X11.clr") or download the file directly [X11.clr.zip](https://raw.githubusercontent.com/nst/BitmapCanvas/master/files/X11.clr.zip) ![X11 Color List](files/X11.clr.png) Other images with sample code: ![gradient](img/gradient.png "gradient") ```Swift let (w, h) = (255, 255) let b = BitmapCanvas(w, h) for i in 0..