Identity verification

iOS SDK - ReadID

845 views December 9, 2019 March 25, 2020 1

Page overview:

Requirements

  • SDK supports iOS 13+(*)
  • SDK supports Xcode 11.0
  • App permission for Camera
  • Device must be NFC compatible

(*) The framework has deployment target iOS 11.0. This enables you to deploy the framework in apps with deployment target >= iOS 11.0. However, all features of the ReadID SDKs have been annotated with @available (iOS 13, *), meaning that these features can only be used in iOS 13 and higher.

Import the SDK

  1. On the General configuration tab of your App Target configuration add ReadID.framework and ReadID_UI.framework and libcrypto.framework to ‘Frameworks, Libraries, and Embedded Content’. Optionally you can use the .xcframework bundles instead of the .framework bundles.
  2. On the Signing & Capabilities configuration tab add the Capability ‘Near Field Communication Tag Reading’
  3. Add ‘NSCameraUsageDescription’ and ‘NSCameraUsageDescription’ to the App’s Info.plist file, since the camera is needed to scan the MRZ
  4. Add ‘NFCReaderUsageDescription’ to the App’s Info.plist file
  5. Copy the snippet below into your list. This is the list of application identifiers that are supported.
    <key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
    <array>
    <string>A0000002471001</string>
    <string>A00000045645444C2D3031</string>
    </array>​
  6. On the Apple developer portal, create a distribution provisioning profile and enable capability ‘NFC Tag Reading’.

Using the SDK

This is what you will need to do during Step D1: Launch provider SDK:

  • Create a new UIViewController that will contain the SDK.
  • Use the “authorization” token (<AUTHORIZATION_TOKEN>) and the “processId” (<PROCESS_ID>) you got during Step C: Create Process.
  • Write the code to handle the scenarios when the user has completed the process, or in case of an error.

The code below provides an example of this:

import UIKit
import ReadID_UI

class ViewController: UIViewController {
   
    //Received result. Pass to ResultViewController.
    var failedReason : FailedReason?
    var mrzResult : MRZResult?
    var nfcResult : NFCResult?

    var configuration : Configuration {
        //Create a Configuration object for general ReadIDUI configuration
        var configuration = Configuration()
       
        configuration.baseUrl = "https://saas-preprod.readid.com:443/odata/v1/ODataServlet/"

        //The authorization you got when creating the process
        configuration.oauthToken = "<AUTHORIZATION_TOKEN>"

        //The processid you got when creating the process
        configuration.opaqueId = "<PROCESS_ID>"

  configuration.isShowInstructionsEnabled = true
        configuration.isShowInstructionsAlwaysEnabled = true
        return configuration
    }
   
    var mrzConfiguration : MRZConfiguration {
        //Create a MRZConfiguration object for specific MRZ configuration
        var mrzConfiguration = MRZConfiguration()
        mrzConfiguration.documentTypeButtons = [.passport, .idCard, .driversLicense]
        mrzConfiguration.allowedDocumentTypes = [.passport, .idCard, .driversLicense]
        mrzConfiguration.mrzResultMode = .none
        mrzConfiguration.isManualInputEnabled = true
        mrzConfiguration.isCheckMRZFieldScoresEnabled = true
        mrzConfiguration.isCheckMRZCheckDigitsEnabled = true
        return mrzConfiguration
    }
   
    var nfcConfiguration : NFCConfiguration {
        //Create a NFCConfiguration object for specific NFC configuration
        var nfcConfiguration = NFCConfiguration()
        nfcConfiguration.documentTypeButtons = [.passport, .idCard, .driversLicense]
        nfcConfiguration.isShowVerificationResultEnabled = true
        return nfcConfiguration
    }
   
    func showReadIDUIViewController() {
        do {
            try ReadIDUI.identify(from: self,
                                  style: .modal(presentationStyle: .automatic),
                                  configuration: configuration,
                                  mrzConfiguration: mrzConfiguration,
                                  nfcConfiguration: nfcConfiguration){
                [weak self]result, mrzResult, nfcResult in
     
                self?.mrzResult = mrzResult
                self?.nfcResult = nfcResult
                self?.failedReason = nil

                switch result {
                case .ok:
                    print(“ok”)
                case .failed(let reason):
                    self?.failedReason = reason
                    self?.performSegue(withIdentifier: "toResults", sender: nil)
                case .closed:
                    break
                case .backNavigation:
                    //Do nothing. User clicked or swiped back.
                    break
                @unknown default:
                    fatalError()
                }
            }
        }catch(let error as ReadIDError){
            switch error {
            case .invalidConfiguration(let msg):
                print("Error in configuration: %@", msg)
            case .internalNFCNotSupported:
                print("NFC not supported for this device")
            @unknown default:
                fatalError()
            }
        }catch(let error){
            print("Error creating ReadIDUI ViewController: %@", error.localizedDescription)
        }
    }
}

Error handling

The code below provides an example for handling the most common errors:

private func errorHandler(error: Error) -> MsgErrorStruct {
        
        if let err = error as? ReadIDError {
            
            switch err {
            case .invalidConfiguration(let msg):
                return MsgErrorStruct(title: "Error in configuration", subTitle: msg)
            case .internalNFCNotSupported:
                return MsgErrorStruct(title: "Your phone don't support NFC", subTitle: "")
            default: break
            }
        }
        
        if let errFailedReason = error as? ReadID_UI.FailedReason {
            
            switch errFailedReason {
            case .backgroundTimeout:
                return MsgErrorStruct(title: "Background Timeout", subTitle: "App was in the background longer than the configured interval")
            case .sessionTimeout:
                return MsgErrorStruct(title: "Session Timeout", subTitle: "Session to the server has timed out")
            case .sessionError:
                return MsgErrorStruct(title: "Session Error", subTitle: "Cannot start session")
            case .connectionSecurityError:
                return MsgErrorStruct(title: "Connection Security", subTitle: "Cannot setup connection, SSL error")
            case .invalidAccessControl:
                return MsgErrorStruct(title: "Cancelled", subTitle: "ReadID cancelled by user when NFC could not be read")
            case .connectionProblem:
                return MsgErrorStruct(title: "Connection Problem", subTitle: "Cannot commit session due to connection problem")
            case .nfcError:
                return MsgErrorStruct(title: "NFC Error", subTitle: "An unrecoverable NFC error occured")
            case .userCancelled:
                return MsgErrorStruct(title: "Cancelled", subTitle: "The user cancelled the reading process")
            case .cameraPermissionDenied:
                return MsgErrorStruct(title: "Camera Permission Denied", subTitle: "The user did not give permission to use the camera")
            }
        }
        
        let text = "Something happened"
        var subTitle = ""
        switch error.code {
        case 4:
            subTitle = "The operation couldn't be completed."
        default:
            subTitle = "\(error.domain): \(error.localizedDescription)"
        }
        return MsgErrorStruct(title: text, subTitle: subTitle) 

}

Customization

The code example above will allow you to use ReadID’s basic flow with their default design. If you want to do some customization, check the next sections to know more about what you can change.

Flow configuration

Show Instructions

Configure the Instruction screen.

Configuration Default Notes
isShowInstructionsEnabled(bool) true Sets showInstructions enabled/disabled.
instructionsReplays(Int) 2 Sets the number of instruction video replays, before the SDK navigates automatically to the next screen. Value <0 replays the instruction video endlessly. It does not have any effect if showInstructionsEnabled is set to false.
isShowInstructionsAlwaysEnabled(bool) false Sets showInstructions always enabled/disabled. If “show instructions always” is not enabled, the instructions will be shown until the user accomplished by the instructions described an action.
isShowInstructionButtonEnabled(bool) true Sets show the instruction button on the MRZ and NFC screen enabled/disabled.

 

Code example:

var configuration : Configuration {
 
        //Create a Configuration object for general ReadIDUI configuration
        var configuration = Configuration()
        ...
        configuration.isShowInstructionsEnabled = true
        configuration.instructionsReplays = 1
        configuration.isShowInstructionsAlwaysEnabled = true
        configuration.isShowInstructionButtonEnabled = false
 
        return configuration
 
}

Image examples:

 

General ReadID UI configuration

The configuration object can be changed to contain other configuration.

Configuration Default Notes
isPinningEnabled(bool) true Sets pinning enabled/disabled. The default value is true. Never set to false in the production app!
dateFormat(ReadID_UI.DateFormat) YYMMDD Possible values are “DDMMYY”, “MMDDYY”, “YYMMDD”.
dateSeparator(ReadID_UI.DateSeparator) hyphens Date separator. Possible values are “hyphens”, “strokes”, “dots”, “spaces”.
backgroundTimeout(TimeInterval) 900 (seconds) Sets after how many seconds in the background the library will automatically return to the caller.
Example: If the app is minimized for more than 900 seconds the sdk will fail and show message “App was in the background longer than the configured interval”
isAskDismissConfirmEnabled(bool) false Sets whether a confirmation is asked before a modally-presented ReadID viewController is dismissed.
isMaskPersonalDataEnabled(bool) false If enabled, the sensitive data (e.g.:document number, personal number) on the result screens will be replaced by *-characters. This has only an effect on the SDK shown data. Not on the data returned to the hosting application.

Code Example:

var configuration : Configuration {
        ...
        configuration.isAskDismissConfirmEnabled = false
        return configuration
    }

MRZ configuration

The MRZConfiguration object can also be changed to contain specific MRZ configuration.

Configuration Default Notes
documentTypeButtons([DocumentType]) [.passport, .idCard] Sets a list of DocumentType for which a button should be shown.
DocumentType.passport is represented by the Passport button.
DocumentType.idCard is represented by the ID card button.
DocumentType.driversLicense is represented by the Driver’s license button.
DocumentType.visa is represented by the Visa button.
DocumentType.vehicleRegistration is represented by the Vehicle registration button.
allowedDocumentTypes([DocumentType]) [ ] Sets a list with allowed DocumentType (same possible values as for documentTypeButtons). Must be set if no document button is shown or to extend the by the documentTypeButtons allowed document types, without showing a button. The first document type determines the shown instruction video/image and the MRZ wireframe if no button is shown.
mrzResultMode(enum.MRZResultMode) .all Sets the MRZ result mode
MRZResultMode.none: skips the result screen
MRZResultMode.accessControl: shows a result screen with access control information (only supported by ICAO compliant documents and electronic driver’s licenses)
MRZResultMode.advancePassengerInformation: shows a result screen with advance passenger information (only supported by ICAO compliant documents)
MRZResultMode.all: shows a result screen with all MRZ information
isShowMRZFieldImagesEnabled(bool) false Sets show MRZ field images enabled/disabled. If set to true for each MRZ field the captured image will be shown.
isShowMRZTextEnabled(bool) false Sets show MRZ text enabled/disabled. If true the detected MRZ text will be shown on the MRZ result screen, otherwise not.
isShowMRZImageEnabled(bool) false Sets show MRZ image enabled/disabled. If true the captured MRZ image will be shown on the MRZ result screen, otherwise not.
isShowVIZImageEnabled(bool) false Sets show VIZ(Visual Inspection Zone) image enabled/disabled. If true the taken VIZ image will be shown on the MRZ result screen, otherwise not.
isShowOCRScanTimeEnabled(bool) false Sets show OCR scan time enabled/disabled. If true the elapsed OCR time will be displayed on the MRZ result screen, otherwise not.
isManualInputEnabled(bool) false Sets manual input enabled/disabled. If manual input is enabled, it is possible to enter the credentials, which are needed to access the NFC chip, using a keyboard. This is useful if an MRZ cannot be scanned, because of e.g. bad light circumstances. The manual input is only available if a document with NFC chip is selected (ICAO Travel Documents and electronic Driver’s licences).
isCheckMRZFieldScoresEnabled(bool) true Sets check MRZ field scores enabled/disabled. If enabled a high score is required to accept the result. A high score means: the matching accumulated character count divided by the accumulated character count (limited by diligence) must be greater than 0.75. Please refer to sections below for more information about diligence and scores.
isCheckMRZCheckDigitsEnabled(bool) true Sets check MRZ check digits enabled/disabled. For more information about check digits, please read this section.
isCheckMRZAssumptionsEnabled(bool) true Sets check MRZ assumptions enabled/disabled. For more information about assumptions, please read this section.

Code example:

var mrzConfiguration : MRZConfiguration {
 
        var mrzConfiguration = MRZConfiguration()
        mrzConfiguration.documentTypeButtons = [.passport, .idCard, .driversLicense]
        mrzConfiguration.allowedDocumentTypes = [.passport, .idCard, .driversLicense]
        mrzConfiguration.mrzResultMode = .none
        mrzConfiguration.isManualInputEnabled = true
        mrzConfiguration.isCheckMRZFieldScoresEnabled = true
        mrzConfiguration.isCheckMRZCheckDigitsEnabled = true
        ...
 
        return mrzConfiguration
 
}

Image examples:

Understanding VIZ and MRZ.

If “isManualInputEnabled” is set to true.

Test with MRZ configurations:

var mrzConfiguration : MRZConfiguration {
 
        ...
        mrzConfiguration.mrzResultMode = .all
        mrzConfiguration.isShowMRZFieldImagesEnabled = true
        mrzConfiguration.isShowMRZTextEnabled = true
        mrzConfiguration.isShowMRZImageEnabled = true
        mrzConfiguration.isShowOCRScanTimeEnabled = true
         
        return mrzConfiguration
    }

Images of the MRZ configurations test.

NFC configuration

The NFCConfiguration object can also be changed to contain a specific NFC configuration.

Configuration Default Notes
nfcAccessKey(MRZResult.nfcAccessKey) The NFC access key can be obtained from MRZResult.nfcAccessKey.
If not set, the user will be asked to manually input the document’s number, the date of birth and the date of expiry – which are the data necessary to decrypt the data in the chip).
If the ReadID API is configured to first read the MRZ and do NFC reading only after that (as i is in the example code above), then it will not be necessary to set an NFC access key (since that info will be retrieved from the MRZ area).
documentTypeButtons([DocumentType]) [.passport, idCard] Sets a list of DocumentType for which a button should be shown.

  • DocumentType.passport is represented by the Passport button
  • DocumentType.idCard and DocumentType.cnis are represented by the ID card button
  • DocumentType.driversLicense is represented by the Driver’s license button

Note: Use only the documentTypes listed above, since other types don’t have a chip.

nfcResultMode(NFCResultMode) .simple Sets the NFC result mode. Possible values:

  • “.all” – All available information from data group 1, 2 and 11 for ICAO compliant documents and 1, 6 and 11 for electronic driver’s licenses will be shown.
  • “.simple” – Basic information from data group 1, 2 and 11 for ICAO complient document and 1, 6 and 11 for electronic driver’s licenses.
  • “.none” – No result screen will be shown
isReadImagesEnabled(bool) true Sets reading of images (face and/or signature) from NFC enabled/disabled

If no images are needed, set to false to speed up reading process.

Impact on Assure API
Note: Disabling this feature will impact the information available through the getImage endpoint of the Assure API.

isShowFaceImageEnabled(bool) true This will display the face image from the document chip. Only used if isReadImagesEnabled = true.
isShowVerificationResultEnabled(bool) true This will display the passive authentication and clone detection result. For more information please read the “Passive Authentication” section below.
isShowSecurityResultEnabled(bool) false Sets show security result enabled/disabled.
skipButtonAttempts(int) 2 Shows a Skip-button on the NFC reading screen after specified number of attempts. The user can use this to skip the NFC reading when he/she has trouble reading the document.

  • -1 Never show Skip-button
  • 0 Show Skip-button immediately
  • >0 Show Skip-button after specified attempts

Code example:

var nfcConfiguration : NFCConfiguration {
 
        var nfcConfiguration = NFCConfiguration() //Create a NFCConfiguration object for specific NFC configuration
        nfcConfiguration.isReadImagesEnabled = true
        nfcConfiguration.documentTypeButtons = [.passport, .idCard, .driversLicense]
        nfcConfiguration.nfcResultMode = .all
        nfcConfiguration.isShowSecurityResultEnabled = true
 
        return nfcConfiguration
 }

Design customization

How to customize UI

ReadID allows overriding the default colors and images. To achieve that you must provide a custom bundle to the ReadIDUI.

If you need custom colors and/or a custom titleImage, set them in your Assets.xcassets. Then set the main bundle as a custom bundle in ReadID:

ReadIDUI.resourcesConfiguration.customBundle = Bundle.main

Example of setting a background color:

Default design

Dark mode

Dark mode feature is available from iOS 13+. When you enable dark mode, the entire UI on your iPhone/iPad changes to a black background and white text.

ReadID will also automatically adapt the interface color depending on what the user has set on their iOS.

You can overwrite what the user defines by adding to the Plist file UIUserInterfaceStyle with the values “Light” or “Dark“.

Background color

Change the background of all screens

Configuration Default
ReadID.backgroundColor #FFFFFF

Background = Green

Text color

Change the color of the text.

Configuration Default
ReadID.textColor #000000

Example of textColor set to orange.

Buttons

Primary button

Configure the design for the primary button.

Configuration Default Notes
ReadID.primaryButtonColor #973189 Background color of primary button in normal state
ReadID.primaryButtonPressedColor #5B1D52 Background color of primary button in pressed state
ReadID.primaryButtonDisabledColor #CCCCCC Background color of primary button in disabled state
ReadID.primaryButtonTextColor #FFFFFF Text color of primary button in normal state
ReadID.primaryButtonPressedTextColor #FFFFFF Text color of primary button in pressed state
ReadID.primaryButtonDisabledTextColor #666666 Text color of primary button in disabled state
ReadID.primaryButtonBorderColor #973189 Border color of primary button in normal state
ReadID.primaryButtonPressedBorderColor #5B1D52 Border color of primary button in pressed state
ReadID.primaryButtonDisabledBorderColor #CCCCCC Border color of primary button in disabled state

Example

Changing the primary button’s colors

Buttons Shape

Configure the buttons size and format.

Configuration
buttonHeight(CGFloat) Height of the buttons.
buttonCornerRadius(CGFloat) Corner radius of the buttons.
buttonTextWeight(UIFont.Weight) Font weight for buttons.
buttonContentInsets(UIEdgeInsets) Extra insets of the buttons.
ReadIDUI.resourcesConfiguration.buttonHeight = 100
ReadIDUI.resourcesConfiguration.buttonCornerRadius = 70
ReadIDUI.resourcesConfiguration.buttonTextWeight = .heavy
ReadIDUI.resourcesConfiguration.buttonStrokeWidth = 20
ReadIDUI.resourcesConfiguration.buttonContentInsets = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 150)

MRZ wire frame

Configure MRZ wire frame colors.

Configuration Default Notes
ReadID.mrzWireframeColor #FFFFFF Color of the wireframe on mrz screen.
ReadID.mrzWireframeOverlayOutsideColor #000000,
50% opacity
Color of background outside of the wireframe on mrz screen.
ReadID.mrzWireframeOverlayInsideColor #000000,
30% opacity
Color of background inside of the wireframe on mrz screen.
ReadID.mrzWireframeOverlayScanAreaColor #000000,
0% opacity
Color of background inside the scan-area of the wireframe on mrz screen.
wireframePadding(CGFloat) Padding around mrz wireframe
wireframeStrokeWidth(CGFloat) Border width of mrz wireframe

Example changing colors of wireframe:

Example with ReadID.mrzWireframeColor = Red | ReadID.mrzWireframeOverlayOutsideColor = Green 50% |  ReadID.mrzWireframeOverlayInsideColor = Purple 30% | ReadID.mrzWireframeOverlayScanAreaColor = Orange 50%

Example changing padding and stroke of wireframe:

ReadIDUI.resourcesConfiguration.wireframePadding = 80
ReadIDUI.resourcesConfiguration.wireframeStrokeWidth = 20
Other color configuration
Configuration Default
ReadID.sectionHeaderTextColor iOS default
ReadID.tabTextColor #973189

Example:

Example with ReadID.sectionHeaderTextColor = Red | ReadID.tabTextColor = Yellow

Font and text

Configure the font type and the text size.

Configuration Default Notes
isDynamicTypeEnabled(bool) True Enable/disable Dynamic Type. When enabled fonts will scale according to the accessibility settings.
textSize(CGFloat) System default size Text size when Dynamic Type is disabled.
textFont(UIFont) System Font Custom font for text.
Changing this setting will require to either set isDynamicTypeEnabled=true or to define the textSize.
buttonFont(UIFont) System Font Custom font for buttons.
Changing this setting will require to either set isDynamicTypeEnabled=true or to define the buttonTextSize.
Does not use buttonTextWeight.
textFieldFont(UIFont) System Font Custom font for textFields.
Changing this setting will require to either set isDynamicTypeEnabled=true or to define the textSize.

Customization test:

        ReadIDUI.resourcesConfiguration.isDynamicTypeEnabled = false
        ReadIDUI.resourcesConfiguration.textFont = UIFont.systemFont(ofSize: 44, weight: UIFont.Weight.semibold)
        ReadIDUI.resourcesConfiguration.buttonFont = UIFont.systemFont(ofSize: 44, weight: UIFont.Weight.thin)
        ReadIDUI.resourcesConfiguration.textFieldFont = UIFont.systemFont(ofSize: 44, weight: UIFont.Weight.heavy)
        ReadIDUI.resourcesConfiguration.sectionHeaderTextSize = 85.0        
        ReadIDUI.resourcesConfiguration.textSize = 55.0
        ReadIDUI.resourcesConfiguration.buttonTextSize = 55.0

Specific ReadID concepts

Check digits

Check digits are a very important aspect when it comes to prevention of OCR mistakes. Even though the allowed character set in an MRZ is very limited, it is not uncommon for example for a number zero to be confused with a capital letter O. To reduce the frequency of these kinds of mistakes from happening, an MRZ contains some so-called check digits. These are digits that are part of the MRZ and are the result of applying a known computation over a subset of the MRZ characters. By default, check digits are enabled, meaning that if the result of this computation does not match the check digit in the OCR result, the result
will not be accepted. For travel documents, the TDData class contains a few helper methods that will tell you if the check digits for document number, date of birth and date of expiry are correct.
Please be aware that what fields in the MRZ are covered by a check digit differs per type of MRZ, e.g., often the name is not covered by a check digit, and date of birth is. Please check the specification for details.

Assumptions

In an ideal situation, the OCR result from ReadID-MRZ exactly matches the text that is printed on the document. But in real life, this is not always the case and not every part of the MRZ is covered by check digits that can be used to discard incorrectly captured results. ReadID-MRZ still makes an effort to avoid this by making use of so-called ‘assumptions’. Assumptions define a set of commonly misinterpreted characters together with the replacement character. The following assumptions are automatically applied:

  • For numeric fields, such as date of birth, letters that resemble a number are replaced by a number (e.g. ‘210A75’ becomes ‘210475’)
  • For non-numeric fields, such as issuing country, numbers that resemble a letter are replaced by a letter (e.g. ‘E5P’ becomes ‘ESP’)
  • For a gender field, characters that resemble a valid gender are replaced by a gender (e.g. ‘E’ becomes ‘F’)

All sub-classes from MRZData also contain helper methods that will tell you if assumptions have been applied during the OCR process. This information can be useful to assess the quality of the OCR result.

Diligence

By default, the OCR engine uses a MEDIUM level diligence, meaning a balanced tradeoff between OCR accuracy and speed. The diligence level can be set to LOW to decrease the time that the OCR engine takes to determine if a valid result is found. This will however also decrease the OCR accuracy. In a normal situation, the OCR engine is already fast enough and there is no need to set the diligence level to LOW. The diligence level can also be set to HIGH, which will increase the likelihood of the captured MRZ to be correct, however this will in turn increase the time it takes the OCR engine to come up with a result. Again, in a normal situation this should not be necessary and the diligence level can be kept at the default value of MEDIUM.

Confidence score

During the OCR process, a confidence score is assigned to every interpreted character. This score is based on the number of times that the engine interpreted that specific character as equal to the final result. A high score means that there was sufficient confidence that the character was properly read. ReadID-MRZ provides helper methods for every field that will tell you if the aggregated score for the field characters is high. Finally, all sub-classes from MRZData also aggregates the scores into a single boolean value indicating that the entire MRZ was very likely not misread.

Passive authentication

The implementation of Passive Authentication varies greatly between passports. The ICAO 9303 standard allows a wide variety of cryptographic algorithms, and different countries also use these in practice. Despite the many differences in the used algorithms, Passive Authentication is always based on the principle of digital signatures and uses a ‘chain of trust’. A country that issues ePassports constitutes a Country Signing Certificate Authority (CSCA), and issues one or more Country Signing Certificates. A Country Signing Certificate is used to cryptographically sign Document Signing Certificates, which in turn can be used to sign the contents of a passport. Consequently, the contents of a document may be considered authentic if signed using a Document Signing Certificate, which in turn has been signed by a trusted Country Signing Certificate.

Was this helpful?