截图清晰度 截图处理 由于笔记截图需要缩放以适应预览视图,所以截图时会出现不清晰的问题。故我们需要用笔记截图的原数据进行渲染绘制。
我们将预览视图分为四大块儿进行处理,分别为,header,note,footer,user info,因为 header 和 footer 只有在设置主题的情况下才可能存在,所以需要将 header,footer 和 user info 分别处理。如下图:
 
 
预览视图分区示意
我们将分别截取 header,footer,user info。note 的截图直接使用 pdfDataAndForcePDFAttachments(toRender: Bool) 生成的 image 数据即可,这样可以保持图片的最高清晰度。
我们对 note 截图进行放大两倍绘制,这样就能非常高清。视网膜屏幕截图的像素宽高默认为图片 size 的两倍。我们这里没有使用 lockFocus() 和 unlockFocus() 方法,是因为这个方法的绘制与当前显示器有关,如果显示器设备不是视网膜屏那么绘制出来的图像则并不清晰。所以我们调用 NSBitmapImageRep 手动创建图像的 bitmap 数据,然后使用 NSGraphicsContext 相关方法进行最终图片的 bitmap 数据生成。
对 header,footer 和 user info 三块儿的截图我们需要将其等比放大到与 note 截图等宽大小,相当于这三块儿内容也进行了放大两倍处理,这样最终效果就会非常清晰。
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 func  screenShot (scale : CGFloat  =  1 ) -> NSImage ? {     guard  scale >  0  else  { return  nil  }   let  targetSize =  NSSize (width: bounds.width *  scale, height: bounds.height *  scale)   guard  let  bitmapRep =  NSBitmapImageRep (bitmapDataPlanes: nil ,                                          pixelsWide: Int (targetSize.width),                                          pixelsHigh: Int (targetSize.height),                                          bitsPerSample: 8 ,                                          samplesPerPixel: 4 ,                                          hasAlpha: true ,                                          isPlanar: false ,                                          colorSpaceName: .calibratedRGB,                                          bytesPerRow: 0 ,                                          bitsPerPixel: 0 ) else  { return  nil  }   NSGraphicsContext .saveGraphicsState()   guard  let  graphicsContext =  NSGraphicsContext (bitmapImageRep: bitmapRep) else  { return  nil  }   NSGraphicsContext .current =  graphicsContext   graphicsContext.cgContext.scaleBy(x: scale, y: scale)   self .displayIgnoringOpacity(bounds, in: graphicsContext)   NSGraphicsContext .restoreGraphicsState()      let  result =  NSImage (size: bitmapRep.size)   result.addRepresentation(bitmapRep)      return  result } 
 
主题处理 当我们设置主题时,最终效果会有一个背景色的添加。此时我们需要对 note 截图 image 数据进行处理,为其添加相应的背景色,代码如下:
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 convenience  init (color : NSColor , img : NSImage , size : NSSize ) {  guard  let  bitmapRep =  NSBitmapImageRep (bitmapDataPlanes: nil ,                                          pixelsWide: Int (size.width),                                          pixelsHigh: Int (size.height),                                          bitsPerSample: 8 ,                                          samplesPerPixel: 4 ,                                          hasAlpha: true ,                                          isPlanar: false ,                                          colorSpaceName: .calibratedRGB,                                          bytesPerRow: 0 ,                                          bitsPerPixel: 0 ) else  {     self .init (size: size)     return    }   NSGraphicsContext .saveGraphicsState()   NSGraphicsContext .current =  NSGraphicsContext (bitmapImageRep: bitmapRep)      color.drawSwatch(in: NSRect (origin: .zero, size: size))   img.draw(in: NSRect (origin: .zero, size: size), from: .zero, operation: .sourceOver, fraction: 1 )      NSGraphicsContext .restoreGraphicsState()      self .init (size: size)   self .addRepresentation(bitmapRep) } 
 
水印处理 当我们设置了水印时,我们先将水印进行截图。然后绘制到最终的效果图上,绘制的 operation 参数一定要是 .sourceOver 类型,如果用 .copy 将会覆盖住最终截图,就好像水印背景并非透明一般。
最终代码如下:
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 func  screenshot () -> NSBitmapImageRep ? {     var  color =  NSColor .white   if  let  bgImage =  theme? .bgImage {     color =  NSColor (patternImage: bgImage)   }      let  noteSize =  NSSize (width: noteScreenshot.size.width *  2.0 , height: noteScreenshot.size.height *  2.0 )   let  noteImage =  NSImage (color: color,                           img: noteScreenshot,                           size: noteSize)      let  scale =  noteSize.width /  bounds.width      var  headerImage: NSImage ?   if  headerHeight >  0 , let  image =  headerView.screenShot(scale: scale) {     headerImage =  NSImage (color: color, img: image, size: image.size)   }      var  footerImage: NSImage ?   if  footerHeight >  0 , let  image =  footerView.screenShot(scale: scale) {     footerImage =  NSImage (color: color, img: image, size: image.size)   }      var  userInfoImage: NSImage ?   if  let  image =  userInfoShowView.screenShot(scale: scale) {     userInfoImage =  NSImage (color: color, img: image, size: image.size)   }      let  watermarkImage =  watermarkView? .screenShot(scale: scale)         let  width =  noteImage.size.width   let  headerHeight =  headerImage? .size.height ??  0    let  footerHeight =  footerImage? .size.height ??  0    let  userInfoHeight =  userInfoImage? .size.height ??  0    let  noteHeight =  noteImage.size.height      let  height =  headerHeight +  noteHeight +  footerHeight +  userInfoHeight      let  targetSize =  NSSize (width: width, height: height)      guard  let  bitmapRep =  NSBitmapImageRep (bitmapDataPlanes: nil ,                                          pixelsWide: Int (targetSize.width),                                          pixelsHigh: Int (targetSize.height),                                          bitsPerSample: 8 ,                                          samplesPerPixel: 4 ,                                          hasAlpha: true ,                                          isPlanar: false ,                                          colorSpaceName: .calibratedRGB,                                          bytesPerRow: 0 ,                                          bitsPerPixel: 0 ) else  { return  nil  }   NSGraphicsContext .saveGraphicsState()   guard  let  context =  NSGraphicsContext (bitmapImageRep: bitmapRep) else  { return  nil  }   NSGraphicsContext .current =  context      if  let  userInfoImage =  userInfoImage {     let  userInfoRect =  NSRect (x: 0 , y: 0 , width: width, height: userInfoHeight)     userInfoImage.draw(in: userInfoRect, from: .zero, operation: .copy, fraction: 1.0 )   }   if  let  footerImage =  footerImage {     let  footerRect =  NSRect (x: 0 , y: userInfoHeight, width: width, height: footerHeight)     footerImage.draw(in: footerRect, from: .zero, operation: .copy, fraction: 1.0 )   }   let  noteRect =  NSRect (x: 0 , y: userInfoHeight +  footerHeight, width: width, height: noteHeight)   noteImage.draw(in: noteRect, from: .zero, operation: .copy, fraction: 1.0 )   if  let  headerImage =  headerImage {     let  headerRect =  NSRect (x: 0 , y: userInfoHeight +  noteHeight +  footerHeight, width: width, height: headerHeight)     headerImage.draw(in: headerRect, from: .zero, operation: .copy, fraction: 1.0 )   }   if  let  watermarkImage =  watermarkImage {     let  watermarkRect =  NSRect (origin: .zero, size: targetSize)     watermarkImage.draw(in: watermarkRect, from: .zero, operation: .sourceOver, fraction: 1.0 )   }      NSGraphicsContext .restoreGraphicsState()      return  bitmapRep } 
 
矩形水印旋转后的间距计算 设计师所给的水印旋转效果如下图:
由于圆形无论怎么旋转,间距是不会变的,所以我们这里可以忽略对圆形水印的间距重计算。矩形旋转之后,对矩形四个顶点做一个外切矩形,外切矩形的宽高是有相应的变化,所以我们需要重新计算矩形的间距。
假设我们以矩形的对角线交点为圆心,画一个外切圆,矩形无论怎么绕着中心点旋转,四个顶点一定在这个外切圆上,并且对角线长度即为这个外切圆的直径(d),从而我们可得外切圆的半径(r)。圆的中心我们作为平面直角坐标系的中心点(0,0),那么我们可以知道矩形四个顶点的坐标。由四个顶点的坐标我们可以算出 d 和 r。根据相应的余弦定理和圆上坐标公式可得旋转后图形的顶点坐标,从而可以计算出旋转之后矩形。然后我们就计算出相应的间距即可。几何坐标表示如下:
图中 ABCD 灰色区域是起始位置,A’B’C’D’ 是逆时针旋转 15° 之后的我们展示水印的位置,灰色的圆为以对角线交点 E 为圆心,对角线长为直径的圆,红色的 FGHI 为旋转 15° 后的外切矩形,也即需要我们计算出宽高的矩形。
首先我们可得 ABCD 四个点的坐标,我们利用 D(xd, yd),B(xb, yb) 两点间距离公式可计算出圆的直径 d,进而可得圆的半径 r。两点间距离公式如下:
我们的最终目标是求出 B‘ 的 y 坐标和 C’ 的 x 坐标,y * 2 即为红色矩形的高,x * 2 即为红色矩形的宽。
因为 B’ 和 C‘ 都在外切圆上,所以利用外切圆的坐标公式即可求出两点坐标,圆上点的坐标公式如下:
圆心 E 的坐标(x0,y0)、半径为r、角度为 angle。
1 2 x1 = x0 + r * cos(angle * PI / 180) y1 = y0 + r * sin(angle * PI / 180) 
 
由公式可知,要想算出 B’ 和 C‘ 坐标,我们需要知道角度 angle。角度的起始位置(0°)为 EJ,所以我们需要计算的角度为 ∠B’EJ(以下称为 α) 和 ∠JEC‘(以下称为 β)。
α 的角度计算 通过图我们可知,α 的大小为等腰三角形 △BEC 两腰的夹角 ∠BEC(以下称为 γ) 的一半再加上 15°。也即 ∠B‘EB + ∠BEJ。两腰的夹角我们可以通过余弦定理算出。余弦定理公式如下:
假设等腰三角形的角为 γ 此时我们可以计算出 cos弧度 的值(macOS 中的旋转是以弧度为单位)。然后我们利用弧度与角度换算公式可知:
1 2 弧度与角度换算公式:1° = π / 180 cos弧度 = cos(γ * PI / 180) 
 
故,具体的角度我们可以用反三角函数 acos 算出。计算方式如下:
1 γ = acos(cos弧度) / (.pi / 180) 
 
旋转角度知道后就可以计算出 B’ 的 y 坐标
1 y = r * sin((γ / 2 + rotationAngle) * .pi / 180) 
 
所以红色矩形的高为 2 * y。
β 的角度计算 C‘ 我们可以看成是 J 顺时针旋转一定的角度得到。则 β 为等腰三角形 △BEC 两腰的夹角 ∠BEC 减去 15° 得到。等腰三角形的夹角 γ 我们已算出,所以 C’ 的 x 坐标我们也很容易得到:
1 2 β = -(γ / 2 - rotationAngle) // 顺时针为负数 x = r * cos(β * .pi / 180) 
 
所以红色矩形的宽为:2 * x
完整计算代码如下:
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 37 38 39 40 41 42 43 44 func  getReal (size  watermarkSize : CGSize , angle  rotationAngle : CGFloat ) -> CGSize  {     var  realSize =  CGSize .zero      if  watermarkSize.width !=  watermarkSize.height {               let  rightTop =  CGPoint (x: watermarkSize.width /  2 , y: watermarkSize.height /  2 )          let  leftBottom =  CGPoint (x: - watermarkSize.width /  2 , y: - watermarkSize.height /  2 )               let  r =  sqrt(pow(rightTop.x -  leftBottom.x, 2 ) +  pow(rightTop.y -  leftBottom.y, 2 )) /  2                     let  cosRadians =  (pow(r, 2 ) *  2  -  pow(watermarkSize.height, 2 )) /  (2  *  r *  r)          let  angle =  acos(cosRadians) /  (.pi /  180 )                              let  rightTopY =  r *  sin((angle /  2  +  rotationAngle) *  .pi /  180 )          let  newHeight =  rightTopY *  2           let  rightBottomRotateAngle =  - (angle /  2  -  rotationAngle)          let  rightBottomX =  r *  cos(rightBottomRotateAngle *  .pi /  180 )          let  newWidth =  rightBottomX *  2      realSize =  CGSize (width: newWidth, height: newHeight)   } else  {     realSize =  watermarkSize   }   return  realSize }