Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loading an image with Styler #258

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Down.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
14C5E33521877CE900D5380C /* DownView.bundle in Resources */ = {isa = PBXBuildFile; fileRef = D41689B51CFFE6BB00E5802B /* DownView.bundle */; };
26CABB93252B4D490032183C /* ChildSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CABB92252B4D490032183C /* ChildSequence.swift */; };
6D553BCC26691C5600CB3C9F /* AsyncImageLoad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D553BCB26691C5600CB3C9F /* AsyncImageLoad.swift */; };
8A569F481E6B3ED2008BE2AC /* DownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43AE5D91CFFD0D0006E1522 /* DownView.swift */; };
8A569F491E6B3ED9008BE2AC /* blocks.c in Sources */ = {isa = PBXBuildFile; fileRef = D4201EF71CFA5D63008EEC6E /* blocks.c */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
8A569F4A1E6B3ED9008BE2AC /* buffer.c in Sources */ = {isa = PBXBuildFile; fileRef = D4201EF81CFA5D63008EEC6E /* buffer.c */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
Expand Down Expand Up @@ -201,6 +202,7 @@
/* Begin PBXFileReference section */
14C5E33621877FCD00D5380C /* DownView (macOS).bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "DownView (macOS).bundle"; sourceTree = "<group>"; };
26CABB92252B4D490032183C /* ChildSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChildSequence.swift; sourceTree = "<group>"; };
6D553BCB26691C5600CB3C9F /* AsyncImageLoad.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncImageLoad.swift; sourceTree = "<group>"; };
8A569F401E6B3E50008BE2AC /* Down.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Down.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8AFAEAFB1E6E32E900E09B68 /* DownTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DownTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
8C73B9B522A687C400C8E60F /* module.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = "<group>"; };
Expand Down Expand Up @@ -583,6 +585,7 @@
children = (
EE5F2BA32262564A00B7C0F3 /* Styler.swift */,
EED8DA8D22BE404F00E54492 /* DownStyler.swift */,
6D553BCB26691C5600CB3C9F /* AsyncImageLoad.swift */,
EE4F77C222FF3F170026A983 /* DownStylerConfiguration.swift */,
);
path = Stylers;
Expand Down Expand Up @@ -958,6 +961,7 @@
EEFFFDDD22F4C8AB00036FD5 /* DownLayoutManager.swift in Sources */,
8A569F771E6B3EE3008BE2AC /* String+ToHTML.swift in Sources */,
8A569F751E6B3EDE008BE2AC /* DownRenderable.swift in Sources */,
6D553BCC26691C5600CB3C9F /* AsyncImageLoad.swift in Sources */,
EEA2BDC622F7057600D0C72C /* ListItemOptions.swift in Sources */,
EE8F38CE22BFEDE50056270E /* ListItemPrefixGenerator.swift in Sources */,
8A569F741E6B3EDE008BE2AC /* DownXMLRenderable.swift in Sources */,
Expand Down
245 changes: 245 additions & 0 deletions Sources/Down/AST/Styling/Stylers/AsyncImageLoad.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
//
// AsyncImageLoad.swift
// Down
//
// Created by Mikhail Ivanov on 03.06.2021.
// Copyright © 2021 Down. All rights reserved.
//

#if canImport(UIKit)

import UIKit

#elseif canImport(AppKit)

import AppKit

#endif

public protocol AsyncImageLoadDelegate
{
func textAttachmentDidLoadImage(textAttachment: AsyncImageLoad, displaySizeChanged: Bool)
}

final public class AsyncImageLoad: NSTextAttachment
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be public?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name suggestion: AsyncImageAttachment

{
public var imageURL: URL?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be optional?


public var displaySize: CGSize?

public var maximumDisplayWidth: CGFloat?

public var delegate: AsyncImageLoadDelegate?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this delegate be ��weak?


weak var textContainer: NSTextContainer?

private var originalImageSize: CGSize?

public init(imageURL: URL? = nil, delegate: AsyncImageLoadDelegate? = nil)
{
self.imageURL = imageURL
self.delegate = delegate
super.init(data: nil, ofType: nil)
}

required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

#if os(macOS)
override public var image: NSImage? {
didSet {
originalImageSize = image?.size
}
}
#else
override public var image: UIImage? {
didSet {
originalImageSize = image?.size
}
}
#endif

// MARK: - Function

private func startAsyncImageDownload()
{
guard let imageURL = imageURL,
contents == nil
else {
return
}

URLSession.shared.dataTask(with: imageURL) { (data, response, error) in

guard let data = data,
error == nil else {
print(error?.localizedDescription as Any)
return
}

var displaySizeChanged = false

self.contents = data

#if os(macOS)
if let image = NSImage(data: data)
{
let imageSize = image.size

if self.displaySize == nil
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the purpose of displaySize to allow an explicit size to be set for the image, rather than it's actual size?

{
displaySizeChanged = true
}

self.originalImageSize = imageSize
}
#else
if let image = UIImage(data: data)
{
let imageSize = image.size

if self.displaySize == nil
{
displaySizeChanged = true
}

self.originalImageSize = imageSize
}
#endif

DispatchQueue.main.async {
if displaySizeChanged
{
self.textContainer?.layoutManager?.setNeedsLayout(forAttachment: self)
}
else
{
self.textContainer?.layoutManager?.setNeedsDisplay(forAttachment: self)
}

// notify the optional delegate
self.delegate?.textAttachmentDidLoadImage(textAttachment: self, displaySizeChanged: displaySizeChanged)
}

}.resume()
}

#if os(macOS)
public override func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> NSImage?
{
if let image = image { return image }

guard let contents = contents, let image = NSImage(data: contents) else
{
self.textContainer = textContainer

startAsyncImageDownload()

return nil
}

return image
}
#else
public override func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> UIImage?
{
if let image = image { return image }

guard let contents = contents, let image = UIImage(data: contents) else
{
self.textContainer = textContainer

startAsyncImageDownload()

return nil
}

return image
}
#endif

public override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect
{
if let displaySize = displaySize
{
return CGRect(origin: CGPoint.zero, size: displaySize)
}

if let imageSize = originalImageSize
{
let maxWidth = maximumDisplayWidth ?? lineFrag.size.width
let factor = maxWidth / imageSize.width

return CGRect(origin: CGPoint.zero, size:CGSize(width: Int(imageSize.width * factor), height: Int(imageSize.height * factor)))
}

return CGRect.zero
}
}

extension NSLayoutManager
{
/// Determine the character ranges for an attachment
private func rangesForAttachment(attachment: NSTextAttachment) -> [NSRange]?
{
guard let attributedString = self.textStorage else
{
return nil
}

// find character range for this attachment
let range = NSRange(location: 0, length: attributedString.length)

var refreshRanges = [NSRange]()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we ever expect to find more than one range for a single attachment?


attributedString.enumerateAttribute(NSAttributedString.Key.attachment, in: range, options: []) { (value, effectiveRange, nil) in

guard let foundAttachment = value as? NSTextAttachment, foundAttachment == attachment else
{
return
}

// add this range to the refresh ranges
refreshRanges.append(effectiveRange)
}

if refreshRanges.count == 0
{
return nil
}

return refreshRanges
}

/// Trigger a relayout for an attachment
public func setNeedsLayout(forAttachment attachment: NSTextAttachment)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be public?

{
guard let ranges = rangesForAttachment(attachment: attachment) else
{
return
}

// invalidate the display for the corresponding ranges
for range in ranges.reversed() {
self.invalidateLayout(forCharacterRange: range, actualCharacterRange: nil)

// also need to trigger re-display or already visible images might not get updated
self.invalidateDisplay(forCharacterRange: range)
}
}

/// Trigger a re-display for an attachment
public func setNeedsDisplay(forAttachment attachment: NSTextAttachment)
{
guard let ranges = rangesForAttachment(attachment: attachment) else
{
return
}

// invalidate the display for the corresponding ranges
for range in ranges.reversed() {
self.invalidateDisplay(forCharacterRange: range)
}
}
}
17 changes: 14 additions & 3 deletions Sources/Down/AST/Styling/Stylers/DownStyler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ open class DownStyler: Styler {
.font: fonts.listItemPrefix,
.foregroundColor: colors.listItemPrefix]
}

private var delegate: AsyncImageLoadDelegate?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name suggestion: asyncImageAttachmentDelegate


// MARK: - Life cycle

public init(configuration: DownStylerConfiguration = DownStylerConfiguration()) {
public init(configuration: DownStylerConfiguration = DownStylerConfiguration(), delegate: AsyncImageLoadDelegate? = nil) {
fonts = configuration.fonts
colors = configuration.colors
paragraphStyles = configuration.paragraphStyles
Expand All @@ -52,6 +54,7 @@ open class DownStyler: Styler {
codeBlockOptions = configuration.codeBlockOptions
itemParagraphStyler = ListItemParagraphStyler(options: configuration.listItemOptions,
prefixFont: fonts.listItemPrefix)
self.delegate = delegate
}

// MARK: - Styling
Expand Down Expand Up @@ -187,8 +190,9 @@ open class DownStyler: Styler {
}

open func style(image str: NSMutableAttributedString, title: String?, url: String?) {
guard let url = url else { return }
styleGenericLink(in: str, url: url)
guard let url = url,
let urlImg = URL(string: url) else { return }
Comment on lines +193 to +194
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion:

guard let imageURL = url.flatMap(URL.init) else { return }

styleGenericImg(in: str, url: urlImg)
}

// MARK: - Common Styling
Expand Down Expand Up @@ -218,6 +222,13 @@ open class DownStyler: Styler {
.link: url,
.foregroundColor: colors.link])
}

private func styleGenericImg(in str: NSMutableAttributedString, url: URL) {
let image1Attachment = AsyncImageLoad(imageURL: url, delegate: delegate)
let image1String = NSAttributedString(attachment: image1Attachment)
Comment on lines +227 to +228
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name suggestions: attachment, and attachmentString


str.setAttributedString(image1String)
}

// MARK: - Helpers

Expand Down