Skip to content
28 changes: 14 additions & 14 deletions Aztec/Classes/TextKit/ImageAttachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,27 +98,27 @@ open class ImageAttachment: MediaAttachment {
}

override func onScreenHeight(_ containerWidth: CGFloat) -> CGFloat {
if let image = image {
let targetWidth = onScreenWidth(containerWidth)
let scale = targetWidth / image.size.width

return floor(image.size.height * scale) + (appearance.imageMargin * 2)
} else {
guard let image = image else {
return 0
}

let targetWidth = onScreenWidth(containerWidth)
let scale = targetWidth / image.size.width

return floor(image.size.height * scale) + (appearance.imageMargin * 2)
}

override func onScreenWidth(_ containerWidth: CGFloat) -> CGFloat {
if let image = image {
switch (size) {
case .full, .none:
return floor(min(image.size.width, containerWidth))
default:
return floor(min(min(image.size.width,size.width), containerWidth))
}
} else {
guard let image = image else {
return 0
}

switch (size) {
case .full, .none:
return floor(min(image.size.width, containerWidth))
default:
return floor(min(min(image.size.width,size.width), containerWidth))
}
}
}

Expand Down
216 changes: 133 additions & 83 deletions Aztec/Classes/TextKit/MediaAttachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ protocol MediaAttachmentDelegate: class {
onSuccess success: @escaping (UIImage) -> (),
onFailure failure: @escaping () -> ())

func mediaAttachmentPlaceholderImageFor(attachment: MediaAttachment) -> UIImage
func mediaAttachmentPlaceholder(for attachment: MediaAttachment) -> UIImage
}

// MARK: - MediaAttachment
Expand Down Expand Up @@ -200,6 +200,13 @@ open class MediaAttachment: NSTextAttachment {
return floor(min(image.size.width, containerWidth))
}

func mediaBounds(for bounds: CGRect) -> CGRect {
let origin = CGPoint(x: xPosition(forContainerWidth: bounds.width), y: appearance.imageMargin)
let size = CGSize(width: onScreenWidth(bounds.width), height: onScreenHeight(bounds.width) - appearance.imageMargin * 2)

return CGRect(origin: origin, size: size)
}


// MARK: - NSTextAttachmentContainer

Expand All @@ -208,7 +215,7 @@ open class MediaAttachment: NSTextAttachment {
ensureImageIsUpToDate(in: textContainer)

guard let image = image else {
return delegate!.mediaAttachmentPlaceholderImageFor(attachment: self)
return delegate!.mediaAttachmentPlaceholder(for: self)
}

if let cachedImage = glyphImage, imageBounds.size.equalTo(cachedImage.size) {
Expand All @@ -220,127 +227,170 @@ open class MediaAttachment: NSTextAttachment {
return glyphImage
}

func mediaBounds(for bounds: CGRect) -> CGRect {
let containerWidth = bounds.size.width
let origin = CGPoint(x: xPosition(forContainerWidth: bounds.size.width), y: appearance.imageMargin)
let size = CGSize(width: onScreenWidth(containerWidth), height: onScreenHeight(containerWidth) - appearance.imageMargin * 2)
return CGRect(origin: origin, size: size)
/// Returns the "Onscreen Character Size" of the attachment range. When we're in Alignment.None,
/// the attachment will be 'Inline', and thus, we'll return the actual Associated View Size.
/// Otherwise, we'll always take the whole container's width.
///
override open func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect {

ensureImageIsUpToDate(in: textContainer)

if image == nil {
return .zero
}

var padding = (textContainer?.lineFragmentPadding ?? 0) * 2
if let storage = textContainer?.layoutManager?.textStorage,
let paragraphStyle = storage.attribute(.paragraphStyle, at: charIndex, effectiveRange: nil) as? NSParagraphStyle {

let attachmentString = storage.attributedSubstring(from: NSMakeRange(charIndex, 1)).string
let headIndent = storage.string.isStartOfParagraph(at: attachmentString.startIndex) ? paragraphStyle.firstLineHeadIndent : paragraphStyle.headIndent

padding += abs(paragraphStyle.tailIndent) + abs(headIndent)
}

let width = floor(lineFrag.width - padding)
let size = CGSize(width: width, height: onScreenHeight(width))

return CGRect(origin: CGPoint.zero, size: size)
}
}


// MARK: - Drawing Methods
//
extension MediaAttachment {

private func glyph(for image: UIImage, in bounds: CGRect) -> UIImage? {
/// Returns the Glyph representing the current image, with all of the required add-ons already embedded:
///
/// - Overlay Background: Whenever there is a message (OR) upload in progress.
/// - Overlay Border: Whenever there is no upload in progress (OR) there is no message visible.
/// - Overlay Image: Image to be displayed at the center of the actual attached image
/// - OVerlay Message: Message to be displayed below the Overlay Image.
/// - Progress Bar: Whenever there's an Upload OP running.
///
func glyph(for image: UIImage, in bounds: CGRect) -> UIImage? {

UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0)

let mediaBounds = self.mediaBounds(for: bounds)
let origin = mediaBounds.origin
let size = mediaBounds.size

image.draw(in: mediaBounds)

drawOverlayBackground(at: origin, size: size)
drawOverlayBorder(at: origin, size: size)
drawProgress(at: origin, size: size)

var imagePadding: CGFloat = 0
if let overlayImage = overlayImage {
UIColor.white.set()
let sizeInsideBorder = CGSize(width: size.width - appearance.overlayBorderWidth, height: size.height - appearance.overlayBorderWidth)
let newImage = overlayImage.resizedImageWithinRect(rectSize: sizeInsideBorder, maxImageSize: overlayImage.size, color: UIColor.white)
let center = CGPoint(x: round(origin.x + (size.width / 2.0)), y: round(origin.y + (size.height / 2.0)))
newImage.draw(at: CGPoint(x: round(center.x - (newImage.size.width / 2.0)), y: round(center.y - (newImage.size.height / 2.0))))
imagePadding += newImage.size.height
}
drawOverlayBackground(in: mediaBounds)
drawOverlayBorder(in: mediaBounds)

if let message = message {
let textRect = message.boundingRect(with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
var y = origin.y + ((size.height - textRect.height) / 2.0)
if imagePadding != 0 {
y = origin.y + Constants.messageTextTopMargin + ((size.height + imagePadding) / 2.0)
}
let textPosition = CGPoint(x: origin.x, y: y)
let overlayImageSize = drawOverlayImage(in: mediaBounds)
drawOverlayMessage(in: mediaBounds, paddingY: overlayImageSize.height)

// Check to see if the message will fit within the image. If not, skip it.
if (textPosition.y + textRect.height) < mediaBounds.height {
message.draw(in: CGRect(origin: textPosition, size: CGSize(width:size.width, height:textRect.size.height)))
}
}
drawProgress(in: mediaBounds)

let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result;

return result
}

private func drawOverlayBackground(at origin: CGPoint, size:CGSize) {

/// Draws an overlay on top of the image, with a color defined by the `appearance.overlayColor` property.
///
private func drawOverlayBackground(in bounds: CGRect) {
guard message != nil || progress != nil else {
return
}
let rect = CGRect(origin: origin, size: size)
let path = UIBezierPath(rect: rect)

let path = UIBezierPath(rect: bounds)
appearance.overlayColor.setFill()
path.fill()
}

private func drawOverlayBorder(at origin: CGPoint, size:CGSize) {
// Don't display the border if the border width is 0, we are force-hiding it, or message is set with no progress
guard appearance.overlayBorderWidth > 0,
shouldHideBorder == false,
progress == nil && message != nil else {
return

/// Draws a border, surroinding the image. It's width will be defined by `appearance.overlayBorderWidth`, while it's color
/// will be taken from `appearance.overlayBorderColor`.
///
/// Note that the `progress` is not nil, or there's an overlay message, this border will not be rendered.
///
private func drawOverlayBorder(in bounds: CGRect) {
guard appearance.overlayBorderWidth > 0, shouldHideBorder == false, progress == nil, message != nil else {
return
}
let rect = CGRect(origin: origin, size: size)
let path = UIBezierPath(rect: rect)

let path = UIBezierPath(rect: bounds)
appearance.overlayBorderColor.setStroke()
path.lineWidth = (appearance.overlayBorderWidth * 2.0)
path.lineWidth = appearance.overlayBorderWidth * 2.0
path.addClip()
path.stroke()
}

private func drawProgress(at origin: CGPoint, size:CGSize) {
guard let progress = progress else {
return

/// Draws the overlayImage at the precise center of the Attachment's bounds.
///
/// - Returns: The actual size of the overlayImage, once displayed onscreen. This size might be actually smaller than the one defined
/// by the actual asset, since we make sure not to render images bigger than the canvas.
///
private func drawOverlayImage(in bounds: CGRect) -> CGSize {
guard let overlayImage = overlayImage else {
return .zero
}
let lineY = origin.y + (appearance.progressHeight / 2.0)

let backgroundPath = UIBezierPath()
backgroundPath.lineWidth = appearance.progressHeight
appearance.progressBackgroundColor.setStroke()
backgroundPath.move(to: CGPoint(x:origin.x, y: lineY))
backgroundPath.addLine(to: CGPoint(x: origin.x + size.width, y: lineY ))
backgroundPath.stroke()
UIColor.white.set()
let sizeInsideBorder = CGSize(width: bounds.width - appearance.overlayBorderWidth, height: bounds.height - appearance.overlayBorderWidth)
let resizedImage = overlayImage.resizedImageWithinRect(rectSize: sizeInsideBorder, maxImageSize: overlayImage.size, color: .white)

let path = UIBezierPath()
path.lineWidth = appearance.progressHeight
appearance.progressColor.setStroke()
path.move(to: CGPoint(x:origin.x, y: lineY))
path.addLine(to: CGPoint(x: origin.x + (size.width * CGFloat(max(0,min(progress,1)))), y: lineY ))
path.stroke()
let overlayOrigin = CGPoint(x: round(bounds.midX - resizedImage.size.width * 0.5),
y: round(bounds.midY - resizedImage.size.height * 0.5))

resizedImage.draw(at: overlayOrigin)

return resizedImage.size
}

/// Returns the "Onscreen Character Size" of the attachment range. When we're in Alignment.None,
/// the attachment will be 'Inline', and thus, we'll return the actual Associated View Size.
/// Otherwise, we'll always take the whole container's width.

/// Draws the Overlay's Message below the overlayImage, at the center of the Attachment.
///
override open func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect {
private func drawOverlayMessage(in bounds: CGRect, paddingY: CGFloat) {
guard let message = message else {
return
}

ensureImageIsUpToDate(in: textContainer)
let textRect = message.boundingRect(with: bounds.size, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
var messageY = bounds.minY + (bounds.height - textRect.height) * 0.5

if image == nil {
return .zero
if paddingY != 0 {
messageY = bounds.minY + Constants.messageTextTopMargin + (bounds.height + paddingY) * 0.5
}

var padding = (textContainer?.lineFragmentPadding ?? 0) * 2
if let storage = textContainer?.layoutManager?.textStorage,
let paragraphStyle = storage.attribute(.paragraphStyle, at: charIndex, effectiveRange: nil) as? NSParagraphStyle {
let attachmentString = storage.attributedSubstring(from: NSMakeRange(charIndex, 1)).string
let headIndent = storage.string.isStartOfParagraph(at: attachmentString.startIndex) ? paragraphStyle.firstLineHeadIndent : paragraphStyle.headIndent
// Check to see if the message will fit within the image. If not, skip it.
let messageRect = CGRect(x: bounds.minX, y: messageY, width: bounds.width, height: textRect.height)
if messageRect.maxY < bounds.height {
message.draw(in: messageRect)
}
}

padding += abs(paragraphStyle.tailIndent) + abs(headIndent)

/// Draws a progress bar, at the top of the image, matching the percentage defined by the ivar `progress`.
///
private func drawProgress(in bounds: CGRect) {
guard let progress = progress else {
return
}
let width = floor(lineFrag.width - padding)

let size = CGSize(width: width, height: onScreenHeight(width))
let progressY = bounds.minY + appearance.progressHeight * 0.5
let progressWidth = bounds.width * CGFloat(max(0, min(progress, 1)))

return CGRect(origin: CGPoint.zero, size: size)
let backgroundPath = UIBezierPath()
backgroundPath.lineWidth = appearance.progressHeight
backgroundPath.move(to: CGPoint(x: bounds.minX, y: progressY))
backgroundPath.addLine(to: CGPoint(x: bounds.maxX, y: progressY))
appearance.progressBackgroundColor.setStroke()
backgroundPath.stroke()

let progressPath = UIBezierPath()
progressPath.lineWidth = appearance.progressHeight
progressPath.move(to: CGPoint(x: bounds.minX, y: progressY))
progressPath.addLine(to: CGPoint(x: bounds.minX + progressWidth, y: progressY))
appearance.progressColor.setStroke()
progressPath.stroke()
}
}

Expand Down Expand Up @@ -372,7 +422,7 @@ private extension MediaAttachment {
return
}

image = delegate!.mediaAttachmentPlaceholderImageFor(attachment: self)
image = delegate!.mediaAttachmentPlaceholder(for: self)
isFetchingImage = true
retryCount += 1

Expand Down Expand Up @@ -430,6 +480,7 @@ private extension MediaAttachment {
/// Constants
///
struct Constants {

/// Maximum number of times to retry downloading the asset, upon error
///
static let maxRetryCount = 3
Expand Down Expand Up @@ -475,8 +526,7 @@ extension MediaAttachment {
///
public var progressColor = UIColor.blue

/// The margin apply to the images being displayed. This is to avoid that two images in a row get
/// glued together.
/// The margin to apply to the images being displayed. This is to avoid that two images in a row get glued together.
///
public var imageMargin = CGFloat(10.0)
}
Expand Down
2 changes: 1 addition & 1 deletion Aztec/Classes/TextKit/TextStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ private extension TextStorage {
//
extension TextStorage: MediaAttachmentDelegate {

func mediaAttachmentPlaceholderImageFor(attachment: MediaAttachment) -> UIImage {
func mediaAttachmentPlaceholder(for attachment: MediaAttachment) -> UIImage {
guard let delegate = attachmentsDelegate else {
fatalError()
}
Expand Down