Make a pdf document by Image using PDFKit in iOS 16 - ios

while we make a pdf using an image file at that we add an image into PDFPage. at that time PDFDocument did not show the original image content in PDFView in iOS 16. I checked the same code which worked properly in iOS 15. For both test cases, I used New Xcode 14, and as well as previously uploaded TestFlight build also worked properly with iOS 15 or below but did not work with iOS 16. So I request to the apple developer who worked on PDFkit please check this case. I may be sure that there is an issue with iOS 16 and I hope apple provides a new iOS 16 sub-version release as soon as possible with includes this bug fix. here is the sample code that I used:-
let images = [UIImage]()
let pdfDocument = PDFDocument()
for (index,image) in images.enumerated(){
// Create a PDF page instance from your image
let pdfPage = PDFPage(image: image)
// Insert the PDF page into your document
pdfDocument.insert(pdfPage!, at: index)
}
Original Image:
PDF preview in PDFView:

My guess is there's an issue with the default color space in PDFKit: RGB vs CMYK
Here's a workaround:
class func imageToPDF(_ image:UIImage) -> Data {
let data = NSMutableData()
let bounds = CGRect(origin: CGPoint.zero, size: image.size)
UIGraphicsBeginPDFContextToData(data, bounds, nil)
UIGraphicsBeginPDFPage()
image.draw(at: CGPoint.zero)
UIGraphicsEndPDFContext()
return data as Data
}
You'll have to alter it for multiple pages in your instance

Try to remove alpha channel from the image before inserting it into PDFPage. Below is an example how to remove alpha channel from UIImage. Tested with iOS 16.1.
extension UIImage {
func removeAlpha() -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.opaque = true
format.scale = scale
return UIGraphicsImageRenderer(size: size, format: format).image { _ in
draw(in: CGRect(origin: .zero, size: size))
}
}
}
struct PhotoDetailView: UIViewRepresentable {
let image: UIImage
func makeUIView(context: Context) -> PDFView {
let view = PDFView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
view.document = PDFDocument()
// {{ REMOVE ALPHA BEFORE INSERTING TO PDF
guard let page = PDFPage(image: image.removeAlpha()) else { return view }
// }}
view.document?.insert(page, at: 0)
view.pageBreakMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
view.minScaleFactor = 1
view.maxScaleFactor = 10
view.autoScales = true
view.backgroundColor = .clear
return view
}
func updateUIView(_ uiView: PDFView, context: Context) {
}
}

Related

Progressview tintColorIssue

I have created a progressview according to number images as you can see in below code.
let view = UIView()
view.backgroundColor = .clear
let progressView = UIProgressView(frame: CGRect(x: 0, y: 0, width: frameOfParentView.width/3 - 8, height: 30))
progressView.progressViewStyle = .default
progressView.progress = 0.0
progressView.tintColor = .red
progressView.trackTintColor = .gray
progressView.layoutIfNeeded()
view.addSubview(progressView)
self.arrayOfProgrssView.append(progressView)
As you can see in gif at starting point tintColor alpha is little bit less but when it tense to reach at 100% it is fully red.
I also tried with below code:-
progressView.progressTintColor = .red
but did not get expected result.
To perform animation,
DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) {
UIView.animate(withDuration: self.animationInMS) {
progressView.setProgress(1, animated: true)
}
}
progressView.layoutIfNeeded()
Issue in iOS 15:-
As you see below result with other colour.
Note:- I have checked in iOS 12.4 it's working properly as you can see into image.
Please let me know is anything require from my side.
Thanks in advance
This does appear to be "new behavior" where the alpha value matches the percent completion -- although, after some quick searching I haven't found any documentation on it.
One option as a work-around: set the .progressImage instead of the tint color.
So, use your favorite code to generate a solid-color image, such as:
extension UIImage {
public static func withColor(_ color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = 1
let image = UIGraphicsImageRenderer(size: size, format: format).image { rendererContext in
color.setFill()
rendererContext.fill(CGRect(origin: .zero, size: size))
}
return image
}
}
Then, instead of:
progressView.tintColor = .red
use:
let img = UIImage.withColor(.red)
progressView.progressImage = img
Not fully tested, but to avoid the need to change existing code, you might also try:
extension UIProgressView {
open override var tintColor: UIColor! {
didSet {
let img = UIImage.withColor(tintColor)
progressImage = img
}
}
}
Now you can keep your existing progressView.tintColor = .red

UIGraphicsImageRenderer generates a strange top padding on iOS 15, working fine on iOS 14.x

iOS 15 generates a strange top padding when I export a SwiftUI View to image using UIGraphicsImageRenderer . It works fine on iOS 14.x . Any one has an idea why?
It turns out to be a real issue of iOS 15.
I posted the same question on Twitter and It has been replied with a working solution.
You can read more about the problem here
The solution is in this gist
I post the gist here for completeness, but all credit goes to its author (not me):
//
// View+Snapshot.swift
//
// Created by Vinzius on 2021-11-06.
//
import SwiftUI
import UIKit.UIImage
import UIKit.UIGraphicsImageRenderer
extension View {
func snapshot() -> UIImage? {
// Note: since iOS 15 it seems these two modifiers are required.
let controller = UIHostingController(
rootView: self.ignoresSafeArea()
.fixedSize(horizontal: true, vertical: true)
)
guard let view = controller.view else { return nil }
let targetSize = view.intrinsicContentSize
if targetSize.width <= 0 || targetSize.height <= 0 { return nil }
view.bounds = CGRect(origin: .zero, size: targetSize)
view.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}

Image Size Changed After Upload To Firebase Storage

imageToUpload is 375x500 here. After uploading to Firebase Storage, the width and height was doubled. Is there a way to keep the size unchanged after uploading to Firebase Storage?
if let uploadData = UIImageJPEGRepresentation(imageToUpload, 0.0) {
uploadImageRef.putData(uploadData, metadata: nil) { (metadata, error) in
if error != nil {
print("error")
completion?(false)
} else {
// your uploaded photo url.
// Metadata contains file metadata such as size, content-type.
let size = metadata?.size
// You can also access to download URL after upload.
uploadImageRef.downloadURL { (url, error) in
guard let downloadURL = url else {
// Uh-oh, an error occurred!
completion?(false)
return
}
print("Download url : \(downloadURL)")
completion?(true)
}
}
}
}
Note that I am using the extension below to change image size to 375x500 (size of imageToUpload) before uploading.
extension UIImage {
func resized(to size: CGSize) -> UIImage {
return UIGraphicsImageRenderer(size: size).image { _ in
draw(in: CGRect(origin: .zero, size: size))
}
}
}
let imageToUploadResize:UIImage = image.resized(to: CGSize(width: 375.0, height: 500.0))
As I had mentioned in my comment, IMO, the function in the extension is really being called like a standalone function and not really extending the functionality. I would suggest making it a function to resize a passed in image and return a new image.
Using this function, your image will be resized to the correct size and uploaded at that size (verified that it works)
func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: targetSize.width, height: targetSize.height)
UIGraphicsBeginImageContextWithOptions(targetSize, false, 1.0)
image.draw(in: rect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
and then called like this
let updatedSize = CGSize(width: 300.0, height: 400.0)
let resizedImage = self.resizeImage(image: origImage!, targetSize: updatedSize)
Now to address the issue with the extension at a 10k foot level.
It all goes back to points vs how it's rendered on an iDevice. Back with the iPhone 2g, 3g, it was 1-1 for rendering so if you ran your code on that device in the simulator and set the image size to 320x480, it would have been a 320x480 image stored in firebase. However, as screens improved and resolutions went up, so did the rendering, which affects the UIImage.
So if you were to set your project to simulate on an iPhone 6, that same image would be 640x960 (2x) and then to iPhone 8+, the size is 960 x 1440 (3x). (there is upsampling involved so we are ignoring it here).
The UIImage knows what device it's on so that should be taken into consideration.
Again, this is generically speaking and there are a lot of other components involved, in particular, pixels = logicalPoints * scaleFactor
Try using the resizing image code provided by the following resource. Your extension creates a new instance of the image and returns it rather than updating the actual image itself.
If you want to keep on using your extension, I would suggest trying
extension UIImage {
func resized(to size: CGSize) {
self = UIGraphicsImageRenderer(size: size).image { _ in
draw(in: CGRect(origin: .zero, size: size))
}
}
}

I don't know how to send screenshot to Instagram application in Swift

I am trying to implement image sharing to Instagram app using Swift.
I do know how to take screenshot but I am unable to get how to share the screenshot to Instagram application.
Here is my snapshot code:
_ = UIScreen.main
if UIApplication.shared.keyWindow != nil {
let top: CGFloat = 101
let bottom: CGFloat = 96
// The size of the cropped image
let size = CGSize(width: view.frame.size.width, height: view.frame.size.height - top - bottom)
// Start the context
UIGraphicsBeginImageContext(size)
// we are going to use context in a couple of places
let context = UIGraphicsGetCurrentContext()!
// Transform the context so that anything drawn into it is displaced "top" pixels up
// Something drawn at coordinate (0, 0) will now be drawn at (0, -top)
// This will result in the "top" pixels being cut off
// The bottom pixels are cut off because the size of the of the context
context.translateBy(x: 0, y: 0)
// Draw the view into the context (this is the snapshot)
UIGraphicsBeginImageContextWithOptions(size,view.isOpaque, 0)
self.view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
let snapshot = UIGraphicsGetImageFromCurrentImageContext()
// End the context (this is required to not leak resources)
UIGraphicsEndImageContext()
Swift 3.x code
You can take screenshot by passing the whichever view you like:
func getScreenshot(viewToSnapShot:UIView) -> UIImage {
UIGraphicsBeginImageContext(viewToSnapShot.frame.size)
viewToSnapShot.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
and grab the documentDirectory path too. I made a simple function that will return the document directory path.
func documentsDirectory() -> String {
return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
}
Now you can share the image on instagram using the UIDocumentInteractionController
In your ViewController import this:
import Social
//make UIDocumentInteractionController object
var docController:UIDocumentInteractionController!
And then call this function whenever you want to post screenshot/image on instagram
// On calling this function, will open your document controller
func postImageToInstagram() {
let instagram = URL(string: "instagram://app")!
if UIApplication.shared.canOpenURL(instagram) {
//By Passing self.view, I am actually taking the screenshot
let imageToBePosted = getScreenshot(viewToSnapShot: self.view)
let imageURL = URL(fileURLWithPath: documentsDirectory())
let fullPath = imageURL.appendingPathComponent("instagram.igo")
do {
try UIImagePNGRepresentation(imageToBePosted)!.write(to: fullPath)
let rect = CGRect(x: 0, y: 0, width: 0, height: 0)
let igImageHookFile = URL(string: "file://\(fullPath)")
docController = UIDocumentInteractionController(url: igImageHookFile!)
docController.uti = "com.instagram.photo"
docController.presentOpenInMenu(from: rect, in: self.view, animated: true)
} catch {
print(error)
}
} else {
print("Instagram not installed")
}
}
Tested on Xcode 8.1, iOS 10.1 device. Worked Fine.

How to capture a full page screenshot of WKWebview?

I can capture all content screenshot of UIWebView by adjust frame of UIScrollView of UIWebView. However, I can't capture all content screenshot of WKWebView by the same method.
The method I used for capture UIWebView as follow:
backup frame and superview of webview.scrollView
create a new container view as webview.scrollView's superview
adjust frame of new container view and webview.scrollView. frame is same to webview.scrollView.contentSize
draw by renderInContext or drawViewHierarchyInRect
However, this method will capture a white screenshot of WKWebview. It doesn't work!
I had print all level of WKWebView, then I found a UIView(WKContentView)'s size is same to contentView, you can found this view by this level:
WKWebView
WKScrollView
WKContentView(size is same to contentView)
I also had try to capture by WKContentView, then I found only visible view could be captured.
Anyway, Anyone could tell me how to capture a full page content screenshot of WKWebView?
Please refer to this answer
iOS 11.0 and above, Apple has provided following API to capture snapshot of WKWebView.
#available(iOS 11.0, *)
open func takeSnapshot(with snapshotConfiguration: WKSnapshotConfiguration?, completionHandler: #escaping (UIImage?, Error?) -> Swift.Void)
Swift 3, Xcode 8
func takeScreenshot() -> UIImage? {
let currentSize = webView.frame.size
let currentOffset = webView.scrollView.contentOffset
webView.frame.size = webView.scrollView.contentSize
webView.scrollView.setContentOffset(CGPoint.zero, animated: false)
let rect = CGRect(x: 0, y: 0, width: webView.bounds.size.width, height: webView.bounds.size.height)
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.main.scale)
webView.drawHierarchy(in: rect, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
webView.frame.size = currentSize
webView.scrollView.setContentOffset(currentOffset, animated: false)
return image
}
Swift 3, Xcode 9:
I created an extension to achieve this. Hope this helps you.
extension WKWebView{
private func stageWebViewForScreenshot() {
let _scrollView = self.scrollView
let pageSize = _scrollView.contentSize;
let currentOffset = _scrollView.contentOffset
let horizontalLimit = CGFloat(ceil(pageSize.width/_scrollView.frame.size.width))
let verticalLimit = CGFloat(ceil(pageSize.height/_scrollView.frame.size.height))
for i in stride(from: 0, to: verticalLimit, by: 1.0) {
for j in stride(from: 0, to: horizontalLimit, by: 1.0) {
_scrollView.scrollRectToVisible(CGRect(x: _scrollView.frame.size.width * j, y: _scrollView.frame.size.height * i, width: _scrollView.frame.size.width, height: _scrollView.frame.size.height), animated: true)
RunLoop.main.run(until: Date.init(timeIntervalSinceNow: 1.0))
}
}
_scrollView.setContentOffset(currentOffset, animated: false)
}
func fullLengthScreenshot(_ completionBlock: ((UIImage) -> Void)?) {
// First stage the web view so that all resources are downloaded.
stageWebViewForScreenshot()
let _scrollView = self.scrollView
// Save the current bounds
let tmp = self.bounds
let tmpFrame = self.frame
let currentOffset = _scrollView.contentOffset
// Leave main thread alone for some time to let WKWebview render its contents / run its JS to load stuffs.
mainDispatchAfter(2.0) {
// Re evaluate the size of the webview
let pageSize = _scrollView.contentSize
UIGraphicsBeginImageContext(pageSize)
self.bounds = CGRect(x: self.bounds.origin.x, y: self.bounds.origin.y, width: pageSize.width, height: pageSize.height)
self.frame = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: pageSize.width, height: pageSize.height)
// Wait few seconds until the resources are loaded
RunLoop.main.run(until: Date.init(timeIntervalSinceNow: 0.5))
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
// reset Frame of view to origin
self.bounds = tmp
self.frame = tmpFrame
_scrollView.setContentOffset(currentOffset, animated: false)
completionBlock?(image)
}
}
}
The key here is to let capture after webkit be allowed time to render after it is given a new frame. I tried didFinishLoading calback and WKWebView's wkWebView.takeSnapshot, but did not work.
So I introduced an a delay for the screenshot:
func createImage(webView: WKWebView, completion: #escaping (UIImage?) -> ()) {
// save the original size to restore
let originalFrame = webView.frame
let originalConstraints = webView.constraints
let originalScrollViewOffset = webView.scrollView.contentOffset
let newSize = webView.scrollView.contentSize
// remove any constraints for the web view, and set the size
// to be size of the content size (will be restored later)
webView.removeConstraints(originalConstraints)
webView.translatesAutoresizingMaskIntoConstraints = true
webView.frame = CGRect(origin: .zero, size: newSize)
webView.scrollView.contentOffset = .zero
// wait for a while for the webview to render in the newly set frame
DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) {
defer {
UIGraphicsEndImageContext()
}
UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
if let context = UIGraphicsGetCurrentContext() {
// render the scroll view's layer
webView.scrollView.layer.render(in: context)
// restore the original state
webView.frame = originalFrame
webView.translatesAutoresizingMaskIntoConstraints = false
webView.addConstraints(originalConstraints)
webView.scrollView.contentOffset = originalScrollViewOffset
if let image = UIGraphicsGetImageFromCurrentImageContext() {
completion(image)
} else {
completion(nil)
}
}
}
}
Result:
- xcode 10 & swift 4.2 -
Use this;
// this is the button which takes the screenshot
#IBAction func snapShot(_ sender: Any) {
captureScreenshot()
}
// this is the function which is the handle screenshot functionality.
func captureScreenshot(){
let layer = UIApplication.shared.keyWindow!.layer
let scale = UIScreen.main.scale
// Creates UIImage of same size as view
UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, scale);
layer.render(in: UIGraphicsGetCurrentContext()!)
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// THIS IS TO SAVE SCREENSHOT TO PHOTOS
UIImageWriteToSavedPhotosAlbum(screenshot!, nil, nil, nil)
}
and you must add these keys in your info.plist;
key value = "Privacy - Photo Library Additions Usage Description" ,
key value = Privacy - Photo Library Usage Description
Here is an iOS 11+ example for snapshotting the WKWebView without the need of any delays to render the content.
The key is to make sure that you dont receive a blank snapshot. Initially we had issues with the output image being blank and having to introduce a delay to have the web content in the output image. I see the solution with the delay in many posts and I would recommend to not use it because it is an unnecessarily unreliable and unperformant solution. The important solution for us was to set the rect of WKSnapshotConfiguration and to use the JavaScript functions to wait for the rendering and also receiving the correct width and height.
Setting up the WKWebView:
// Important: You can set a width here which will also be reflected in the width of the snapshot later
let webView = WKWebView(frame: .zero)
webView.configuration.dataDetectorTypes = []
webView.navigationDelegate = self
webView.scrollView.contentInsetAdjustmentBehavior = .never
webView.loadHTMLString(htmlString, baseURL: Bundle.main.resourceURL)
Capturing the snapshot:
func webView(
_ webView: WKWebView,
didFinish navigation: WKNavigation!
) {
// Wait for the page to be rendered completely and get the final size from javascript
webView.evaluateJavaScript("document.readyState", completionHandler: { [weak self] (readyState, readyStateError) in
webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: {(contentWidth, widthError) in
webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (contentHeight, heightError) in
guard readyState != nil,
let contentHeight = contentHeight as? CGFloat,
let contentWidth = contentWidth as? CGFloat else {
[Potential error handling...]
return
}
let rect = CGRect(
x: 0,
y: 0,
width: contentWidth,
height: contentHeight
)
let configuration = WKSnapshotConfiguration()
configuration.rect = rect
if #available(iOS 13.0, *) {
configuration.afterScreenUpdates = true
}
webView.takeSnapshot(with: configuration) { (snapshotImage, error) in
guard let snapshotImage = snapshotImage else {
[Potential error handling...]
}
[Do something with the image]
}
})
})
})
}
UPDATE: Not sure why Jason is sticking this answer all over the net. IT DOES NOT solve the problem of HOW to Screenshot the Full WKWebView content... including that off the screen.
Try this:
func snapshot() -> UIImage {
UIGraphicsBeginImageContextWithOptions(self.webView.bounds.size, true, 0);
self.webView.drawViewHierarchyInRect(self.webView.bounds, afterScreenUpdates: true);
let snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageWriteToSavedPhotosAlbum(snapshotImage, nil, nil, nil)
return snapshotImage
}
The image will automatically save into the iOS Camera Roll (by UIImageWriteToSavedPhotosAlbum()).

Resources