Data Providers


PSPDFKit supports loading data from many different sources. PSPDFDataProvider is especially helpful if you want to support your own encryption or compression scheme.

It allows you to define how to read and how to write data to it, making it very customizable and giving you the freedom to store your data in the exact way you need it.

Existing Data Providers

PSPDFKit ships with two pre-made PSPDFDataProvider classes.

Custom Data Providers

You can also write your own, custom data provider and pass it along to documentWithDataProvider:.

Read Support

To provide read support, you have to implement the PSPDFDataProvider protocol. It offers methods to read the data at a specific offset and to uniquely identify the content.

Here's a simple example of how to implement it to read from a NSData instance.

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
class YourDataProvider: NSObject, NSSecureCoding, PSPDFDataProvider {

    // MARK: Properties

    var size: UInt64 {
        // Returns the size of the data.
        guard let data = self.data else { return 0 }
        return UInt64(data.count)
    }

    var uid: String {
        // This can be anything that uniquely identifies your data.
        // Resource-name from the original data, UUID, you name it.
        return uniqueIdentifierForYourData
    }

    // [...]

    // MARK: PSPDFDataProvider

    func readData(withSize size: UInt64, atOffset offset: UInt64) -> Data {
        guard let data = self.data else { return Data() }
        // We have to clamp the given size and offset to make sure we don't try
        // to read data that doesn't exist.
        let length = self.size
        let clampedOffset = min(offset, length)
        let clampedSize = min(size, length - clampedOffset)
        // Actually return the data.
        let range: Range = Int(clampedOffset)..<Int(clampedOffset+clampedSize)
        return data.subdata(in: range)
    }
}
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
@interface YourDataProvider : NSObject <PSPDFDataProvider> @end

@implementation YourDataProvider

#pragma mark - Properties

- (uint64_t)size {
    // Returns the size of the data.
    return self.data.length;
}

- (NSString *)UID {
    // This can be anything that uniquely identifies your data.
    // Resource-name from the original data, UUID, you name it.
    return uniqueIdentifierForYourData;
}

// [...]

#pragma mark - PSPDFDataProvider

- (NSData *)readDataWithSize:(uint64_t)size atOffset:(uint64_t)offset {
    // We have to clamp the given size and offset to make sure we don't try
    // to read data that doesn't exist.
    const NSUInteger length = self.size;
    NSUInteger clampedOffset = MIN((NSUInteger)offset, length);
    NSUInteger clampedSize =  MIN((NSUInteger)size, length - clampedOffset);
    // Actually return the data.
    return [self.data subdataWithRange:NSMakeRange(clampedOffset, clampedSize)];
}

@end

Write Support

Write support is a little more difficult than simply offering a write method. The reason for this is that PSPDFKit actually has to be able to read the document while writing it, which means we can't simply overwrite data.

For this reason, we introduced the concept of a PSPDFDataSink. It supports the following options (PSPDFDataSinkOptions):

  • PSPDFDataSinkOptionNone - the writes that are incoming are from the beginning of the file (this is the default option).
  • PSPDFDataSinkOptionAppend - the writes that are incoming should be appended to the file.

To support writing, we first need to write a PSPDFDataSink.

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
class YourDataSink: NSObject, PSPDFDataSink {

    // MARK: Properties

    private(set) var isFinished = false
    let options: PSPDFDataSinkOptions
    var writtenData = Data()

    // MARK: Lifecycle

    init(options: PSPDFDataSinkOptions) {
        self.options = options
        super.init()
    }

    // MARK: PSPDFDataSink

    func write(_ data: Data) -> Bool {
        // We append the passed in data to our writtenData.
        writtenData.append(data)
        return true
    }

    func finish() -> Bool {
        // If you're implementing compression or encryption writing, you might need
        // to tell the compression or encryption library that you are finished
        // writing. You can do this here. For our purposes with the `NSData`, we
        // don't need to do anything.
        isFinished = true
        return true
    }
}
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
@interface YourDataSink : NSObject <PSPDFDataSink>

@property (nonatomic, readonly) PSPDFDataSinkOptions options;
@property (nonatomic, readonly) NSMutableData *writtenData;

@end

@interface YourDataSink ()

@property (nonatomic) BOOL isFinished;

@end

@implementation YourDataSink

- (instancetype)initWithOptions:(PSPDFDataSinkOptions)options {
    if ((self = [super init])) {
        // We initialize `writtenData` with a empty, mutable `NSMutableData`
        _writtenData = [NSMutableData data];
        _options = options;
    }
    return self;
}

- (BOOL)writeData:(NSData *)data {
    // We append the passed in data to our writtenData.
    [self.writtenData appendData:data];
    return YES;
}

- (BOOL)finish {
    // If you're implementing compression or encryption writing, you might need
    // to tell the compression or encryption library that you are finished
    // writing. You can do this here. For our purposes with the `NSData`, we
    // don't need to do anything.
    self.isFinished = YES;
    return YES;
}

@end

This is a basic PSPDFDataSink implementation that simply writes to the passed in NSData. In order for the PSPDFDataProvider to make use of it, we have to extend it just a little:

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
class YourDataProvider: NSObject, NSSecureCoding, PSPDFDataProvider {

    // ... your previous implementation ...

    var additionalOperationsSupported: PSPDFDataProviderAdditionalOperations {
        // Signal to PSPDFKit that this data provider can support writing by
        // returning the following option:
        return .write
    }

    func createDataSink(options: PSPDFDataSinkOptions) -> PSPDFDataSink {
        // When PSPDFKit wants to write to the data provider, it will call this
        // method and passes in if it wants to overwrite or append to the file.
        return YourDataSink(options: options)
    }

    func replace(with replacementDataSink: PSPDFDataSink) -> Bool {
        // After PSPDFKit finishes writing, it passes in the data sink
        // that was previously created in `-createDataSinkWithOptions:`.
        guard let dataSink = replacementDataSink as? YourDataSink else { return false }

        // We have to check if we have to overwrite or append
        if dataSink.options.contains(.append) {
            // We have to append the data to ours
            guard let data = data else { return false }
            var replacementData = data
            replacementData.append(dataSink.writtenData)
        } else {
            // We can simply replace our data
            data = dataSink.writtenData
        }

        return true
    }
}
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
@implementation YourNSDataProvider

// ... your previous implementation ...

- (PSPDFDataProviderAdditionalOperations)additionalOperationsSupported {
    // Signal to PSPDFKit that this data provider can support writing by
    // returning the following option:
    return PSPDFDataProviderAdditionalOperationWrite;
}

- (id<PSPDFDataSink>)createDataSinkWithOptions:(PSPDFDataSinkOptions)options {
    // When PSPDFKit wants to write to the data provider, it will call this
    // method and passes in if it wants to overwrite or append to the file.
    return [[YourNSDataSink alloc] initWithOptions:options];
}

- (BOOL)replaceWithDataSink:(id<PSPDFDataSink>)replacementDataSink {
    // After PSPDFKit finishes writing, it passes in the data sink
    // that was previously created in `-createDataSinkWithOptions:`.
    YourNSDataSink *dataSink = (YourNSDataSink*)replacementDataSink;
    // We have to check if we have to overwrite or append
    if (dataSink.options & PSPDFDataSinkOptionAppend) {
        // We have to append the data to ours
        NSMutableData *replacementData = [self.data mutableCopy];
        [replacementData appendData:dataSink.writtenData];
        self.data = replacementData;
    } else {
        // We can simply replace our data
        self.data = dataSink.writtenData;
    }
    return YES;
}

@end

Always remember that even while writing, the PSPDFDataProvider must be able to fully read the document.

Was this page helpful? We're happy to answer any questions.