<?php
if ( ! defined( 'ABSPATH' ) ) exit;

// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fopen
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fread
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fclose

if ( ! class_exists( 'WOE_FPDF_TT_Font_File' ) ) {
	require( 'class-woe-fpdf-tt-font-file.php' );
}

if ( ! class_exists( 'WOE_FPDF_Exception' ) ) {
	require( 'exception/class-woe-fpdf-exception.php' );
}

class WOE_FPDF {

	const ORIENTATION_PORTRAIT = 'P';
	const ORIENTATION_LANDSCAPE = 'L';

	const DOCUMENT_STATE_NOT_INITIALIZED = 0;
	const DOCUMENT_STATE_INITIALIZED = 1;
	const DOCUMENT_STATE_CREATING = 2;
	const DOCUMENT_STATE_TERMINATED = 3;

	const FONT_TRUETYPE = 'TTF';

	const FILE_FONT_METRICS = 'fm-';
	const FILE_CHARACTER_WIDTH = 'cw-';

	/**
	 * @var bool
	 */
	protected $bol_uniform_subset = false;

	/**
	 * Current page number
	 *
	 * @var int
	 */
	protected $int_page = 0;

	/**
	 * Current object number
	 *
	 * @var int
	 */
	protected $int_current_object = 2;

	/**
	 * Array of object offsets
	 *
	 * @var array
	 */
	protected $arr_offsets = [];

	/**
	 * Buffer holding in-memory PDF
	 *
	 * @var string
	 */
	protected $str_buffer = '';

	/**
	 * Array containing pages
	 *
	 * @var array
	 */
	protected $arr_pages = [];

	/**
	 * Current document state
	 *
	 * @var int
	 */
	protected $int_state = self::DOCUMENT_STATE_NOT_INITIALIZED;

	/**
	 * Compression flag
	 *
	 * @var bool
	 */
	protected $bol_compress = false;

	/**
	 * Scale factor (number of points in user unit)
	 *
	 * @var int
	 */
	protected $flt_scale_factor = 1;

	/**
	 * Default orientation
	 *
	 * @var string
	 */
	protected $str_default_orientation = '';

	/**
	 * Current orientation
	 *
	 * @var string
	 */
	protected $str_current_orientation = '';

	/**
	 * Standard page sizes
	 *
	 * @var array
	 */
	protected $arr_standard_page_sizes = [
		'a3'     => array( 841.89, 1190.55 ),
		'a4'     => array( 595.28, 841.89 ),
		'a5'     => array( 420.94, 595.28 ),
		'letter' => array( 612, 792 ),
		'legal'  => array( 612, 1008 )
	];

	/**
	 * Default page size
	 *
	 * @var array|string
	 */
	protected $arr_default_page_sizes = [];

	/**
	 * Current page size
	 *
	 * @var array|string
	 */
	protected $arr_current_page_sizes = [];

	/**
	 * Used for pages with non default sizes or orientations
	 *
	 * @var array
	 */
	protected $arr_page_sizes = [];

	/**
	 * Dimensions of current page in points
	 *
	 * @var mixed
	 */
	protected $flt_width_points, $flt_height_points = 0.00;

	/**
	 * Dimensions of current page in user units
	 *
	 * @var mixed
	 */
	protected $flt_current_width, $flt_current_height = 0.00;

	/**
	 * Left Margin
	 *
	 * @var int
	 */
	protected $int_left_margin = 0;

	/**
	 * Top Margin
	 *
	 * @var int
	 */
	protected $int_top_margin = 0;

	/**
	 * Right Margin
	 *
	 * @var int
	 */
	protected $int_right_margin = 0;

	/**
	 * Page break margin
	 *
	 * @var int
	 */
	protected $int_break_margin = 0;

	/**
	 * Cell Margin
	 *
	 * @var float|int
	 */
	protected $int_cell_margin = 0;

	/**
	 * Current position in user unit
	 *
	 * @var
	 */
	protected $flt_position_x, $flt_position_y = 0.00;

	/**
	 * Height of last printed cell
	 *
	 * @var float
	 */
	protected $flt_last_cell_height = 0.00;

	/**
	 * Line width in user units
	 *
	 * @var float
	 */
	protected $flt_line_width = 0.00;

	/**
	 * The path containing fonts
	 *
	 * @var string
	 */
	protected $str_font_path = '';

	/**
	 * Array of core font names
	 *
	 * @var array
	 */
	protected $arr_core_fonts = [
		'courier',
		'helvetica',
		'times',
		'symbol',
		'zapfdingbats'
	];

	/**
	 * Array of used fonts
	 *
	 * @var array
	 */
	protected $arr_fonts = [];

	/**
	 * Array of font files
	 *
	 * @var array
	 */
	protected $arr_font_files = [];

	/**
	 * Array of encoding differences
	 *
	 * @var array
	 */
	protected $arr_encoding_diffs = [];

	/**
	 * Current font family
	 *
	 * @var string
	 */
	protected $str_current_font_family = '';

	/**
	 * Current font style
	 *
	 * @var string
	 */
	protected $str_current_font_style = '';

	/**
	 * Underline flag
	 *
	 * @var bool
	 */
	protected $bol_underline = false;

	/**
	 * Array of current font info
	 *
	 * @var array
	 */
	protected $arr_current_font_info = [];

	/**
	 * Current font size in points
	 *
	 * @var int
	 */
	protected $int_current_font_size = 12;

	/**
	 * Current font size in user units
	 *
	 * @var int
	 */
	protected $int_font_size_user = 0;

	/**
	 * The draw color
	 *
	 * @var string
	 */
	protected $str_draw_color = '0 G';

	/**
	 * The fill color
	 *
	 * @var string
	 */
	protected $str_fill_color = '0 g';

	/**
	 * The text color
	 *
	 * @var string
	 */
	protected $str_text_color = '0 g';

	/**
	 * Indicates whether fill and text colors are different
	 *
	 * @var bool
	 */
	protected $bol_fill_text_differ = false;

	/**
	 * The word spacing
	 *
	 * @var int
	 */
	protected $int_word_spacing = 0;

	/**
	 * Array of used images
	 *
	 * @var array
	 */
	protected $arr_images = [];

	/**
	 * Array of links in pages
	 *
	 * @var array
	 */
	protected $arr_page_links = [];

	/**
	 * Array of internal links
	 *
	 * @var array
	 */
	protected $arr_internal_links = [];

	/**
	 * Automatic page breaking
	 *
	 * @var bool
	 */
	protected $bol_auto_page_break = false;

	/**
	 * Threshold used to trigger page breaks
	 *
	 * @var float
	 */
	protected $flt_page_break_trigger = 0.00;

	/**
	 * Flag set when processing header
	 *
	 * @var bool
	 */
	protected $bol_in_header = false;

	/**
	 * Flag set when processing footer
	 *
	 * @var bool
	 */
	protected $bol_in_footer = false;

	/**
	 * Zoom display mode
	 *
	 * @var mixed
	 */
	protected $mix_zoom_mode;

	/**
	 * Layout display mode
	 *
	 * @var string
	 */
	protected $str_layout_mode = '';

	/**
	 * The title
	 *
	 * @var string
	 */
	protected $str_title = '';

	/**
	 * The subject
	 *
	 * @var string
	 */
	protected $str_subject = '';

	/**
	 * The author
	 *
	 * @var string
	 */
	protected $str_author = '';

	/**
	 * The keywords
	 *
	 * @var string
	 */
	protected $str_keywords = '';

	/**
	 * The creator
	 *
	 * @var string
	 */
	protected $str_creator = '';

	/**
	 * The alias for total number of pages
	 *
	 * @var string
	 */
	protected $str_alias_number_pages = '';

	/**
	 * The PDF version number
	 *
	 * @var string
	 */
	protected $str_pdf_version = '1.3';

	/**
	 * The font metric cache directory, if null no cache will be used
	 *
	 * @var string
	 */
	protected $cachePath = null;


	/**
	 * PDF constructor.
	 *
	 * @param string $str_orientation
	 * @param string $str_units
	 * @param string $str_size
	 */
	public function __construct( $str_orientation = 'P', $str_units = 'mm', $str_size = 'A4' ) {

		$this->setFontPath( __DIR__ . '/font' );

		// Scale factor
		switch ( $str_units ) {
			case 'pt':
				$this->flt_scale_factor = 1;
				break;
			case 'mm':
			case 'cm':
				$this->flt_scale_factor = 72 / 25.4;
				break;
			case 'in':
				$this->flt_scale_factor = 72;
				break;
			default:
				throw new WOE_FPDF_Exception( esc_html('Invalid unit specified: ' . $str_units), esc_html(WOE_FPDF_Exception::INVALID_UNIT) );
		}

		$str_size                     = $this->getPageSize( $str_size );
		$this->arr_default_page_sizes = $str_size;
		$this->arr_current_page_sizes = $str_size;
		// Page orientation

		switch ( $str_orientation ) {

			case 'portrait':
			case self::ORIENTATION_PORTRAIT:
				$this->str_default_orientation = 'P';
				$this->flt_current_width       = $str_size[0];
				$this->flt_current_height      = $str_size[1];
				break;

			case 'landscape':
			case self::ORIENTATION_LANDSCAPE:
				$this->str_default_orientation = 'L';
				$this->flt_current_width       = $str_size[1];
				$this->flt_current_height      = $str_size[0];
				break;

			default:
				throw new WOE_FPDF_Exception( esc_html('Invalid orientation: ' . $str_orientation), esc_html(WOE_FPDF_Exception::INVALID_ORIENTATION) );
				break;
		}

		$this->str_current_orientation = $this->str_default_orientation;
		$this->flt_width_points        = $this->flt_current_width * $this->flt_scale_factor;
		$this->flt_height_points       = $this->flt_current_height * $this->flt_scale_factor;

		// Page margins (1 cm)
		$flt_margin = 28.35 / $this->flt_scale_factor;
		$this->setMargins( $flt_margin, $flt_margin );

		// Interior cell margin (1 mm)
		$this->int_cell_margin = $flt_margin / 10;

		// Line width (0.2 mm)
		$this->flt_line_width = .567 / $this->flt_scale_factor;

		// Automatic page break
		$this->SetAutoPageBreak( true, 2 * $flt_margin );

		// Default display mode
		$this->SetDisplayMode( 'default' );

		// Enable compression
		$this->SetCompression( true );
	}

	/**
	 * @param      $int_left
	 * @param      $int_top
	 * @param null $int_right
	 */
	public function setMargins( $int_left, $int_top, $int_right = null ) {
		// Set left, top and right margins
		$this->int_left_margin = $int_left;
		$this->int_top_margin  = $int_top;
		if ( null === $int_right ) {
			$int_right = $int_left;
		}
		$this->int_right_margin = $int_right;
	}

	/**
	 * @param $int_margin
	 */
	public function SetLeftMargin( $int_margin ) {
		// Set left margin
		$this->int_left_margin = $int_margin;
		if ( $this->int_page > 0 && $this->flt_position_x < $int_margin ) {
			$this->flt_position_x = $int_margin;
		}
	}

	/**
	 * @param $int_margin
	 */
	public function SetTopMargin( $int_margin ) {
		// Set top margin
		$this->int_top_margin = $int_margin;
	}

	/**
	 * @param $int_margin
	 */
	public function SetRightMargin( $int_margin ) {
		// Set right margin
		$this->int_right_margin = $int_margin;
	}

	/**
	 * @param     $bol_auto
	 * @param int $int_margin
	 */
	public function SetAutoPageBreak( $bol_auto, $int_margin = 0 ) {
		// Set auto page break mode and triggering margin
		$this->bol_auto_page_break    = $bol_auto;
		$this->int_break_margin       = $int_margin;
		$this->flt_page_break_trigger = $this->flt_current_height - $int_margin;
	}

	/**
	 * Set display mode and zoom view
	 *
	 * @param string $zoomMode Set zoom mode: default, fullpage, fullwidth or real
	 * @param string $layoutMode Set layout mode: default, single, continuous or two
	 */
	public function SetDisplayMode( $zoomMode = null, $layoutMode = null ) {

		// Validate zoom mode
		$zoomModes = [ 'default', 'fullpage', 'fullwidth', 'real' ];
		if ( $zoomMode === null ) {
			$zoomMode = $zoomModes[0];
		}

		if ( ! in_array( $zoomMode, $zoomModes ) ) {
			throw new WOE_FPDF_Exception( esc_html('Invalid zoom mode specified: `' . $zoomMode . '`'), esc_html(WOE_FPDF_Exception::INVALID_ZOOM_MODE) );
		}

		// Valid layout mode
		$layoutModes = [ 'default', 'single', 'continuous', 'two' ];
		if ( $layoutMode === null ) {
			$layoutMode = $layoutModes[0];
		}

		if ( ! in_array( $layoutMode, $layoutModes ) ) {
			throw new WOE_FPDF_Exception( esc_html('Invalid layout mode specified: `' . $layoutMode . '`'), esc_html(WOE_FPDF_Exception::INVALID_LAYOUT_MODE) );
		}

		// Set zoom and layout modes
		$this->mix_zoom_mode   = $zoomMode;
		$this->str_layout_mode = $layoutMode;
	}

	/**
	 * @param $bol_compression
	 */
	public function SetCompression( $bol_compression ) {
		// Set page compression
		if ( function_exists( 'gzcompress' ) ) {
			$this->bol_compress = $bol_compression;
		} else {
			$this->bol_compress = false;
		}
	}

	/**
	 * @param      $str_title
	 * @param bool $bol_utf8
	 */
	public function SetTitle( $str_title, $bol_utf8 = false ) {
		// Title of document
		if ( $bol_utf8 ) {
			$str_title = $this->UTF8toUTF16( $str_title );
		}
		$this->str_title = $str_title;
	}

	/**
	 * @param      $str_subject
	 * @param bool $bol_utf8
	 */
	public function SetSubject( $str_subject, $bol_utf8 = false ) {
		// Subject of document
		if ( $bol_utf8 ) {
			$str_subject = $this->UTF8toUTF16( $str_subject );
		}
		$this->str_subject = $str_subject;
	}

	/**
	 * @param      $str_author
	 * @param bool $bol_utf8
	 */
	public function SetAuthor( $str_author, $bol_utf8 = false ) {
		// Author of document
		if ( $bol_utf8 ) {
			$str_author = $this->UTF8toUTF16( $str_author );
		}
		$this->str_author = $str_author;
	}

	/**
	 * @param      $str_keywords
	 * @param bool $bol_utf8
	 */
	public function SetKeywords( $str_keywords, $bol_utf8 = false ) {
		// Keywords of document
		if ( $bol_utf8 ) {
			$str_keywords = $this->UTF8toUTF16( $str_keywords );
		}
		$this->str_keywords = $str_keywords;
	}

	/**
	 * @param      $str_creator
	 * @param bool $bol_utf8
	 */
	public function SetCreator( $str_creator, $bol_utf8 = false ) {
		// Creator of document
		if ( $bol_utf8 ) {
			$str_creator = $this->UTF8toUTF16( $str_creator );
		}
		$this->str_creator = $str_creator;
	}

	/**
	 * @param string $str_alias
	 */
	public function AliasNbPages( $str_alias = '{nb}' ) {
		// Define an alias for total number of pages
		$this->str_alias_number_pages = $str_alias;
	}

	/**
	 * Inits the document
	 */
	public function Open() {
		// Begin document
		$this->int_state = self::DOCUMENT_STATE_INITIALIZED;
	}

	/**
	 * Closes the document
	 */
	public function Close() {
		// Terminate document
		if ( $this->int_state == self::DOCUMENT_STATE_TERMINATED ) {
			return;
		}
		if ( $this->int_page == 0 ) {
			$this->AddPage();
		}
		// Page footer
		$this->bol_in_footer = true;
		$this->Footer();
		$this->bol_in_footer = false;
		// Close page
		$this->EndPage();
		// Close document
		$this->EndDoc();
	}

	/**
	 * @param string $str_orientation
	 * @param string $str_size
	 */
	public function AddPage( $str_orientation = '', $str_size = '' ) {
		// Start a new page
		if ( $this->int_state == self::DOCUMENT_STATE_NOT_INITIALIZED ) {
			$this->Open();
		}
		$str_font_family      = $this->str_current_font_family;
		$str_style            = $this->str_current_font_style . ( $this->bol_underline ? 'U' : '' );
		$int_font_size        = $this->int_current_font_size;
		$flt_line_width       = $this->flt_line_width;
		$str_draw_color       = $this->str_draw_color;
		$str_fill_color       = $this->str_fill_color;
		$str_text_color       = $this->str_text_color;
		$bol_fill_text_differ = $this->bol_fill_text_differ;
		if ( $this->int_page > 0 ) {
			// Page footer
			$this->bol_in_footer = true;
			$this->Footer();
			$this->bol_in_footer = false;
			// Close page
			$this->EndPage();
		}
		// Start new page
		$this->BeginPage( $str_orientation, $str_size );
		// Set line cap style to square
		$this->Out( '2 J' );
		// Set line width
		$this->flt_line_width = $flt_line_width;
		$this->Out( sprintf( '%.2F w', $flt_line_width * $this->flt_scale_factor ) );
		// Set font
		if ( $str_font_family ) {
			$this->SetFont( $str_font_family, $str_style, $int_font_size );
		}
		// Set colors
		$this->str_draw_color = $str_draw_color;
		if ( $str_draw_color != '0 G' ) {
			$this->Out( $str_draw_color );
		}
		$this->str_fill_color = $str_fill_color;
		if ( $str_fill_color != '0 g' ) {
			$this->Out( $str_fill_color );
		}
		$this->str_text_color       = $str_text_color;
		$this->bol_fill_text_differ = $bol_fill_text_differ;
		// Page header
		$this->bol_in_header = true;
		$this->Header();
		$this->bol_in_header = false;
		// Restore line width
		if ( $this->flt_line_width != $flt_line_width ) {
			$this->flt_line_width = $flt_line_width;
			$this->Out( sprintf( '%.2F w', $flt_line_width * $this->flt_scale_factor ) );
		}
		// Restore font
		if ( $str_font_family ) {
			$this->SetFont( $str_font_family, $str_style, $int_font_size );
		}
		// Restore colors
		if ( $this->str_draw_color != $str_draw_color ) {
			$this->str_draw_color = $str_draw_color;
			$this->Out( $str_draw_color );
		}
		if ( $this->str_fill_color != $str_fill_color ) {
			$this->str_fill_color = $str_fill_color;
			$this->Out( $str_fill_color );
		}
		$this->str_text_color       = $str_text_color;
		$this->bol_fill_text_differ = $bol_fill_text_differ;
	}

	/**
	 *
	 */
	public function Header() {
		// To be implemented in your own inherited class
	}

	/**
	 *
	 */
	public function Footer() {
		// To be implemented in your own inherited class
	}

	/**
	 * @return int
	 */
	public function PageNo() {
		// Get current page number
		return $this->int_page;
	}

	/**
	 * @param      $int_red
	 * @param null $int_green
	 * @param null $int_blue
	 */
	public function SetDrawColor( $int_red, $int_green = null, $int_blue = null ) {
		// Set color for all stroking operations
		if ( ( $int_red == 0 && $int_green == 0 && $int_blue == 0 ) || $int_green === null ) {
			$this->str_draw_color = sprintf( '%.3F G', $int_red / 255 );
		} else {
			$this->str_draw_color = sprintf( '%.3F %.3F %.3F RG', $int_red / 255, $int_green / 255, $int_blue / 255 );
		}
		if ( $this->int_page > 0 ) {
			$this->Out( $this->str_draw_color );
		}
	}

	/**
	 * @param      $int_red
	 * @param null $int_green
	 * @param null $int_blue
	 */
	public function SetFillColor( $int_red, $int_green = null, $int_blue = null ) {
		// Set color for all filling operations
		if ( ( $int_red == 0 && $int_green == 0 && $int_blue == 0 ) || $int_green === null ) {
			$this->str_fill_color = sprintf( '%.3F g', $int_red / 255 );
		} else {
			$this->str_fill_color = sprintf( '%.3F %.3F %.3F rg', $int_red / 255, $int_green / 255, $int_blue / 255 );
		}
		$this->bol_fill_text_differ = ( $this->str_fill_color != $this->str_text_color );
		if ( $this->int_page > 0 ) {
			$this->Out( $this->str_fill_color );
		}
	}

	/**
	 * @param      $int_red
	 * @param null $int_green
	 * @param null $int_blue
	 */
	public function SetTextColor( $int_red, $int_green = null, $int_blue = null ) {
		// Set color for text
		if ( ( $int_red == 0 && $int_green == 0 && $int_blue == 0 ) || $int_green === null ) {
			$this->str_text_color = sprintf( '%.3F g', $int_red / 255 );
		} else {
			$this->str_text_color = sprintf( '%.3F %.3F %.3F rg', $int_red / 255, $int_green / 255, $int_blue / 255 );
		}
		$this->bol_fill_text_differ = ( $this->str_fill_color != $this->str_text_color );
	}

	/**
	 * @param $str_text
	 *
	 * @return float|int
	 */
	public function GetStringWidth( $str_text ) {
		// Get width of a string in the current font
		$str_text            = (string) $str_text;
		$arr_character_width = &$this->arr_current_font_info['cw'];
		$flt_width           = 0;
		if ( $this->bol_uniform_subset ) {
			$str_unicode = $this->UTF8StringToArray( $str_text );
			foreach ( $str_unicode as $str_char ) {
				if ( isset( $arr_character_width[ $str_char ] ) ) {
					$flt_width += ( ord( $arr_character_width[ 2 * $str_char ] ) << 8 ) + ord( $arr_character_width[ 2 * $str_char + 1 ] );
				} else {
					if ( $str_char > 0 && $str_char < 128 && isset( $arr_character_width[ chr( $str_char ) ] ) ) {
						$flt_width += $arr_character_width[ chr( $str_char ) ];
					} else {
						if ( isset( $this->arr_current_font_info['desc']['MissingWidth'] ) ) {
							$flt_width += $this->arr_current_font_info['desc']['MissingWidth'];
						} else {
							if ( isset( $this->arr_current_font_info['MissingWidth'] ) ) {
								$flt_width += $this->arr_current_font_info['MissingWidth'];
							} else {
								$flt_width += 500;
							}
						}
					}
				}
			}
		} else {
			$int_length = strlen( $str_text );
			for ( $i = 0; $i < $int_length; $i ++ ) {
				$flt_width += $arr_character_width[ $str_text[ $i ] ];
			}
		}

		return $flt_width * $this->int_font_size_user / 1000;
	}

	/**
	 * @param $flt_width
	 */
	public function SetLineWidth( $flt_width ) {
		// Set line width
		$this->flt_line_width = $flt_width;
		if ( $this->int_page > 0 ) {
			$this->Out( sprintf( '%.2F w', $flt_width * $this->flt_scale_factor ) );
		}
	}

	/**
	 * @param $flt_x_1
	 * @param $flt_y_1
	 * @param $flt_x_2
	 * @param $flt_y_2
	 */
	public function Line( $flt_x_1, $flt_y_1, $flt_x_2, $flt_y_2 ) {
		// Draw a line
		$this->Out( sprintf( '%.2F %.2F m %.2F %.2F l S', $flt_x_1 * $this->flt_scale_factor, ( $this->flt_current_height - $flt_y_1 ) * $this->flt_scale_factor, $flt_x_2 * $this->flt_scale_factor, ( $this->flt_current_height - $flt_y_2 ) * $this->flt_scale_factor ) );
	}

	/**
	 * @param        $flt_x
	 * @param        $flt_y
	 * @param        $flt_width
	 * @param        $flt_height
	 * @param string $str_style
	 */
	public function Rect( $flt_x, $flt_y, $flt_width, $flt_height, $str_style = '' ) {
		// Draw a rectangle
		if ( $str_style == 'F' ) {
			$op = 'f';
		} elseif ( $str_style == 'FD' || $str_style == 'DF' ) {
			$op = 'B';
		} else {
			$op = 'S';
		}
		$this->Out( sprintf( '%.2F %.2F %.2F %.2F re %s', $flt_x * $this->flt_scale_factor, ( $this->flt_current_height - $flt_y ) * $this->flt_scale_factor, $flt_width * $this->flt_scale_factor, - $flt_height * $this->flt_scale_factor, $op ) );
	}

	/**
	 * Add a unicode font to the document
	 *
	 * @param string $fontFamily The Font-Family name to be used in setFont method
	 * @param string $fontStyle Font-Style of the font to be used in setFont method (B, I, U)
	 * @param string $fontFile The relative font filename used with the set font path
	 */
	public function AddFont( $fontFamily, $fontStyle = '', $fontFile = '' ) {

		// Add a TrueType, OpenType or Type1 font
		$fontFamily = strtolower( $fontFamily );
		$fontStyle  = strtoupper( $fontStyle );

		if ( $fontStyle == 'IB' ) {
			$fontStyle = 'BI';
		}
		if ( $fontFile == '' ) {
			$fontFile = str_replace( ' ', '', $fontFamily ) . strtolower( $fontStyle );

			if ( file_exists( $fontFile . '.ttf' ) ) {
				$fontFile .= '.ttf';
			} else {
				$fontFile .= '.php';
			}
		}

		$fontExtension = pathinfo( $fontFile, PATHINFO_EXTENSION );
		$isUnicode     = ( strtolower( $fontExtension ) != 'php' );

		$fontKey = $fontFamily . $fontStyle;
		if ( isset( $this->arr_fonts[ $fontKey ] ) ) {
			return;
		}

		if ( $isUnicode ) {
			$fontFile = $this->getFontPath() . $fontFile;

			$fontMetrics = $this->getFontMetricFiles( $fontFile, $fontKey );

			$int_font_count = count( $this->arr_fonts ) + 1;
			$arr_numbers    = range( 0, ( ! empty( $this->str_alias_number_pages ) ) ? 57 : 32 );

			$this->arr_fonts[ $fontKey ] = array_merge( $fontMetrics, [
				'i'        => $int_font_count,
				'ttffile'  => $fontFile,
				'subset'   => $arr_numbers,
				'filename' => pathinfo( $fontFile, PATHINFO_FILENAME ),
			] );

			$this->arr_font_files[ $fontKey ]  = [
				'length1' => $fontMetrics['originalsize'],
				'type'    => self::FONT_TRUETYPE,
				'ttffile' => $fontFile,
			];
			$this->arr_font_files[ $fontFile ] = [
				'type' => self::FONT_TRUETYPE
			];

			unset( $fontMetrics );
		} else {
			$arr_info      = $this->LoadFont( $fontFile );
			$arr_info['i'] = count( $this->arr_fonts ) + 1;
			if ( ! empty( $arr_info['diff'] ) ) {
				// Search existing encodings
				$int_key = array_search( $arr_info['diff'], $this->arr_encoding_diffs );
				if ( ! $int_key ) {
					$int_key                              = count( $this->arr_encoding_diffs ) + 1;
					$this->arr_encoding_diffs[ $int_key ] = $arr_info['diff'];
				}
				$arr_info['diffn'] = $int_key;
			}
			if ( ! empty( $arr_info['file'] ) ) {
				// Embedded font
				if ( $arr_info['type'] == 'TrueType' ) {
					$this->arr_font_files[ $arr_info['file'] ] = array( 'length1' => $arr_info['originalsize'] );
				} else {
					$this->arr_font_files[ $arr_info['file'] ] = array(
						'length1' => $arr_info['size1'],
						'length2' => $arr_info['size2']
					);
				}
			}
			$this->arr_fonts[ $fontKey ] = $arr_info;
		}
	}

	/**
	 * @param $fontFile
	 * @param $fontKey
	 *
	 * @return array
	 */
	public function getFontMetricFiles( $fontFile, $fontKey ) {

		$ext      = strrchr( $fontFile, '.' );
		$baseName = basename( $fontFile, $ext );

		$cachePath = $this->getCachePath();

		$metricFile    = $cachePath . self::FILE_FONT_METRICS . $baseName . '.json';
		$charWidthFile = $cachePath . self::FILE_CHARACTER_WIDTH . $baseName . '.dat';

		$oFileSize = filesize( $fontFile );

		if ( $cachePath !== null && file_exists( $metricFile ) && file_exists( $charWidthFile ) ) {
			$fopen = fopen( $metricFile, 'r' );
			$fread = fread( $fopen, filesize( $metricFile ) + 1 );
			fclose( $fopen );

			$json = json_decode( $fread, true );

			if ( json_last_error() === JSON_ERROR_NONE ) {
				$fopen = fopen( $charWidthFile, 'r' );
				$fread = fread( $fopen, filesize( $charWidthFile ) + 1 );
				fclose( $fopen );

				$json['cw'] = $fread;

				if ( $oFileSize == $json['originalsize'] ) {
					return $json;
				}
			}
		}

		$ttf = new WOE_FPDF_TT_Font_File();
		$ttf->getMetrics( $fontFile );

		$name = preg_replace( '/[ ()]/', '', $ttf->getFullName() );

		$flt_underline_pos       = round( $ttf->getUnderlinePosition() );
		$flt_underline_thickness = round( $ttf->getUnderlineThickness() );

		$characterWidths = $ttf->getCharWidths();

		// Generate metrics array
		$strMetricsData = [
			'name'                    => $name,
			'type'                    => self::FONT_TRUETYPE,
			'desc'                    => [
				'Ascent'       => round( $ttf->getAscent() ),
				'Descent'      => round( $ttf->getDescent() ),
				'CapHeight'    => round( $ttf->getCapHeight() ),
				'Flags'        => $ttf->getFlags(),
				'FontBBox'     => '[' . round( $ttf->getBbox()[0] ) . ' ' . round( $ttf->getBbox()[1] ) . ' ' . round( $ttf->getBbox()[2] ) . ' ' . round( $ttf->getBbox()[3] ) . ']',
				'ItalicAngle'  => $ttf->getItalicAngle(),
				'StemV'        => round( $ttf->getStemV() ),
				'MissingWidth' => round( $ttf->getDefaultWidth() ),
			],
			'flt_underline_pos'       => $flt_underline_pos,
			'flt_underline_thickness' => $flt_underline_thickness,
			'ttffile'                 => str_replace( __DIR__ . "/", "", $metricFile ),
			'originalsize'            => $oFileSize,
			'fontkey'                 => $fontKey,
			'cw'                      => null,
		];

		$cachePath = $this->getCachePath();
		if ( $cachePath !== null ) {

			// write metrics file
			$json = json_encode( $strMetricsData );

			$fopen = fopen( $metricFile, 'w' );
			fwrite( $fopen, $json );
			fclose( $fopen );

			// write char width file
			$fopen = fopen( $charWidthFile, 'w' );
			fwrite( $fopen, $characterWidths );
			fclose( $fopen );

			// wp_delete_file char width 127 file
			$charWidth127file = $cachePath . self::FILE_CHARACTER_WIDTH . $baseName . '.json';
			if ( file_exists( $charWidth127file ) ) {
				@wp_delete_file( $charWidth127file );
			}
		}

		$strMetricsData['cw'] = $characterWidths;

		unset( $ttf );

		return $strMetricsData;
	}

	/**
	 * @param        $str_family
	 * @param string $str_style
	 * @param int    $int_size
	 */
	public function SetFont( $str_family, $str_style = '', $int_size = 0 ) {
		// Select a font; size given in points
		if ( $str_family == '' ) {
			$str_family = $this->str_current_font_family;
		} else {
			$str_family = strtolower( $str_family );
		}
		$str_style = strtoupper( $str_style );
		if ( strpos( $str_style, 'U' ) !== false ) {
			$this->bol_underline = true;
			$str_style           = str_replace( 'U', '', $str_style );
		} else {
			$this->bol_underline = false;
		}
		if ( $str_style == 'IB' ) {
			$str_style = 'BI';
		}
		if ( $int_size == 0 ) {
			$int_size = $this->int_current_font_size;
		}
		// Test if font is already selected
		if ( $this->str_current_font_family == $str_family && $this->str_current_font_style == $str_style && $this->int_current_font_size == $int_size ) {
			return;
		}
		// Test if font is already loaded
		$str_font_key = $str_family . $str_style;
		if ( ! isset( $this->arr_fonts[ $str_font_key ] ) ) {
			// Test if one of the core fonts
			if ( $str_family == 'arial' ) {
				$str_family = 'helvetica';
			}
			if ( ! in_array( $str_family, $this->arr_core_fonts ) ) {
				throw new WOE_FPDF_Exception( esc_html('Undefined font: ' . $str_family . ' ' . $str_style), esc_html(WOE_FPDF_Exception::UNDEFINED_FONT) );
			}

			if ( $str_family == 'symbol' || $str_family == 'zapfdingbats' ) {
				$str_style = '';
			}
			$str_font_key = $str_family . $str_style;
			if ( ! isset( $this->arr_fonts[ $str_font_key ] ) ) {
				$this->AddFont( $str_family, $str_style );
			}
		}
		// Select it
		$this->str_current_font_family = $str_family;
		$this->str_current_font_style  = $str_style;
		$this->int_current_font_size   = $int_size;
		$this->int_font_size_user      = $int_size / $this->flt_scale_factor;
		$this->arr_current_font_info   = &$this->arr_fonts[ $str_font_key ];
		if ( $this->arr_fonts[ $str_font_key ]['type'] == self::FONT_TRUETYPE ) {
			$this->bol_uniform_subset = true;
		} else {
			$this->bol_uniform_subset = false;
		}
		if ( $this->int_page > 0 ) {
			$this->Out( sprintf( 'BT /F%d %.2F Tf ET', $this->arr_current_font_info['i'], $this->int_current_font_size ) );
		}
	}

	/**
	 * @param $int_size
	 */
	public function SetFontSize( $int_size ) {
		// Set font size in points
		if ( $this->int_current_font_size == $int_size ) {
			return;
		}
		$this->int_current_font_size = $int_size;
		$this->int_font_size_user    = $int_size / $this->flt_scale_factor;
		if ( $this->int_page > 0 ) {
			$this->Out( sprintf( 'BT /F%d %.2F Tf ET', $this->arr_current_font_info['i'], $this->int_current_font_size ) );
		}
	}

	/**
	 * @return int
	 */
	public function AddLink() {
		// Create a new internal link
		$int_count                              = count( $this->arr_internal_links ) + 1;
		$this->arr_internal_links[ $int_count ] = array( 0, 0 );

		return $int_count;
	}

	/**
	 * @param     $mix_link_key
	 * @param int $int_y
	 * @param int $int_page
	 */
	public function SetLink( $mix_link_key, $int_y = 0, $int_page = - 1 ) {
		// Set destination of internal link
		if ( $int_y == - 1 ) {
			$int_y = $this->flt_position_y;
		}
		if ( $int_page == - 1 ) {
			$int_page = $this->int_page;
		}
		$this->arr_internal_links[ $mix_link_key ] = array( $int_page, $int_y );
	}

	/**
	 * @param $flt_x
	 * @param $flt_y
	 * @param $flt_width
	 * @param $flt_height
	 * @param $mix_link_key
	 */
	public function Link( $flt_x, $flt_y, $flt_width, $flt_height, $mix_link_key ) {
		// Put a link on the page
		$this->arr_page_links[ $this->int_page ][] = array(
			$flt_x * $this->flt_scale_factor,
			$this->flt_height_points - $flt_y * $this->flt_scale_factor,
			$flt_width * $this->flt_scale_factor,
			$flt_height * $this->flt_scale_factor,
			$mix_link_key
		);
	}

	/**
	 * @param $flt_x
	 * @param $flt_y
	 * @param $str_text
	 */
	public function Text( $flt_x, $flt_y, $str_text ) {
		// Output a string
		if ( $this->bol_uniform_subset ) {
			$str_text_2 = '(' . $this->EscapeString( $this->UTF8ToUTF16BE( $str_text, false ) ) . ')';
			foreach ( $this->UTF8StringToArray( $str_text ) as $uni ) {
				$this->arr_current_font_info['subset'][ $uni ] = $uni;
			}
		} else {
			$str_text_2 = '(' . $this->EscapeString( $str_text ) . ')';
		}
		$str_output = sprintf( 'BT %.2F %.2F Td %s Tj ET', $flt_x * $this->flt_scale_factor, ( $this->flt_current_height - $flt_y ) * $this->flt_scale_factor, $str_text_2 );
		if ( $this->bol_underline && $str_text != '' ) {
			$str_output .= ' ' . $this->DoUnderline( $flt_x, $flt_y, $str_text );
		}
		if ( $this->bol_fill_text_differ ) {
			$str_output = 'q ' . $this->str_text_color . ' ' . $str_output . ' Q';
		}
		$this->Out( $str_output );
	}

	/**
	 * @return bool
	 */
	public function AcceptPageBreak() {
		// Accept automatic page break or not
		return $this->bol_auto_page_break;
	}

	/**
	 * @param float  $flt_width
	 * @param float  $flt_height
	 * @param string $str_text
	 * @param int    $int_border
	 * @param int    $int_line_number
	 * @param string $str_alignment
	 * @param bool   $bol_fill
	 * @param string $str_link
	 */
	public function Cell( $flt_width, $flt_height = 0, $str_text = '', $int_border = 0, $int_line_number = 0, $str_alignment = '', $bol_fill = false, $str_link = '' ) {
		// Output a cell
		$flt_scale = $this->flt_scale_factor;
		if ( $this->flt_position_y + $flt_height > $this->flt_page_break_trigger && ! $this->bol_in_header && ! $this->bol_in_footer && $this->AcceptPageBreak() ) {
			// Automatic page break
			$flt_position_x   = $this->flt_position_x;
			$int_word_spacing = $this->int_word_spacing;
			if ( $int_word_spacing > 0 ) {
				$this->int_word_spacing = 0;
				$this->Out( '0 Tw' );
			}
			$this->AddPage( $this->str_current_orientation, $this->arr_current_page_sizes );
			$this->flt_position_x = $flt_position_x;
			if ( $int_word_spacing > 0 ) {
				$this->int_word_spacing = $int_word_spacing;
				$this->Out( sprintf( '%.3F Tw', $int_word_spacing * $flt_scale ) );
			}
		}
		if ( $flt_width == 0 ) {
			$flt_width = $this->flt_current_width - $this->int_right_margin - $this->flt_position_x;
		}
		$str_output = '';
		if ( $bol_fill || $int_border == 1 ) {
			if ( $bol_fill ) {
				$str_operation = ( $int_border == 1 ) ? 'B' : 'f';
			} else {
				$str_operation = 'S';
			}
			$str_output = sprintf( '%.2F %.2F %.2F %.2F re %s ', $this->flt_position_x * $flt_scale, ( $this->flt_current_height - $this->flt_position_y ) * $flt_scale, $flt_width * $flt_scale, - $flt_height * $flt_scale, $str_operation );
		}
		if ( is_string( $int_border ) ) {
			$flt_position_x = $this->flt_position_x;
			$flt_position_y = $this->flt_position_y;
			if ( strpos( $int_border, 'L' ) !== false ) {
				$str_output .= sprintf( '%.2F %.2F m %.2F %.2F l S ', $flt_position_x * $flt_scale, ( $this->flt_current_height - $flt_position_y ) * $flt_scale, $flt_position_x * $flt_scale, ( $this->flt_current_height - ( $flt_position_y + $flt_height ) ) * $flt_scale );
			}
			if ( strpos( $int_border, 'T' ) !== false ) {
				$str_output .= sprintf( '%.2F %.2F m %.2F %.2F l S ', $flt_position_x * $flt_scale, ( $this->flt_current_height - $flt_position_y ) * $flt_scale, ( $flt_position_x + $flt_width ) * $flt_scale, ( $this->flt_current_height - $flt_position_y ) * $flt_scale );
			}
			if ( strpos( $int_border, 'R' ) !== false ) {
				$str_output .= sprintf( '%.2F %.2F m %.2F %.2F l S ', ( $flt_position_x + $flt_width ) * $flt_scale, ( $this->flt_current_height - $flt_position_y ) * $flt_scale, ( $flt_position_x + $flt_width ) * $flt_scale, ( $this->flt_current_height - ( $flt_position_y + $flt_height ) ) * $flt_scale );
			}
			if ( strpos( $int_border, 'B' ) !== false ) {
				$str_output .= sprintf( '%.2F %.2F m %.2F %.2F l S ', $flt_position_x * $flt_scale, ( $this->flt_current_height - ( $flt_position_y + $flt_height ) ) * $flt_scale, ( $flt_position_x + $flt_width ) * $flt_scale, ( $this->flt_current_height - ( $flt_position_y + $flt_height ) ) * $flt_scale );
			}
		}
		if ( $str_text !== '' ) {
			if ( $str_alignment == 'R' ) {
				$flt_dimensions = $flt_width - $this->int_cell_margin - $this->GetStringWidth( $str_text );
			} elseif ( $str_alignment == 'C' ) {
				$flt_dimensions = ( $flt_width - $this->GetStringWidth( $str_text ) ) / 2;
			} else {
				$flt_dimensions = $this->int_cell_margin;
			}
			if ( $this->bol_fill_text_differ ) {
				$str_output .= 'q ' . $this->str_text_color . ' ';
			}

			// If multibyte, Tw has no effect - do word spacing using an adjustment before each space
			if ( $this->int_word_spacing && $this->bol_uniform_subset ) {
				foreach ( $this->UTF8StringToArray( $str_text ) as $mix_unicode ) {
					$this->arr_current_font_info['subset'][ $mix_unicode ] = $mix_unicode;
				}
				$str_space     = $this->EscapeString( $this->UTF8ToUTF16BE( ' ', false ) );
				$str_output    .= sprintf( 'BT 0 Tw %.2F %.2F Td [', ( $this->flt_position_x + $flt_dimensions ) * $flt_scale, ( $this->flt_current_height - ( $this->flt_position_y + .5 * $flt_height + .3 * $this->int_font_size_user ) ) * $flt_scale );
				$arr_bits      = explode( ' ', $str_text );
				$int_bit_count = count( $arr_bits );
				for ( $i = 0; $i < $int_bit_count; $i ++ ) {
					$str_converted_text = $arr_bits[ $i ];
					$str_converted_text = '(' . $this->EscapeString( $this->UTF8ToUTF16BE( $str_converted_text, false ) ) . ')';
					$str_output         .= sprintf( '%s ', $str_converted_text );
					if ( ( $i + 1 ) < $int_bit_count ) {
						$flt_adjustment = - ( $this->int_word_spacing * $this->flt_scale_factor ) * 1000 / $this->int_current_font_size;
						$str_output     .= sprintf( '%d(%s) ', $flt_adjustment, $str_space );
					}
				}
				$str_output .= '] TJ';
				$str_output .= ' ET';
			} else {
				if ( $this->bol_uniform_subset ) {
					$str_text_2 = '(' . $this->EscapeString( $this->UTF8ToUTF16BE( $str_text, false ) ) . ')';
					foreach ( $this->UTF8StringToArray( $str_text ) as $mix_unicode ) {
						$this->arr_current_font_info['subset'][ $mix_unicode ] = $mix_unicode;
					}
				} else {
					$str_text_2 = '(' . str_replace( ')', '\\)', str_replace( '(', '\\(', str_replace( '\\', '\\\\', $str_text ) ) ) . ')';
				}
				$str_output .= sprintf( 'BT %.2F %.2F Td %s Tj ET', ( $this->flt_position_x + $flt_dimensions ) * $flt_scale, ( $this->flt_current_height - ( $this->flt_position_y + .5 * $flt_height + .3 * $this->int_font_size_user ) ) * $flt_scale, $str_text_2 );
			}
			if ( $this->bol_underline ) {
				$str_output .= ' ' . $this->DoUnderline( $this->flt_position_x + $flt_dimensions, $this->flt_position_y + .5 * $flt_height + .3 * $this->int_font_size_user, $str_text );
			}
			if ( $this->bol_fill_text_differ ) {
				$str_output .= ' Q';
			}
			if ( $str_link ) {
				$this->Link( $this->flt_position_x + $flt_dimensions, $this->flt_position_y + .5 * $flt_height - .5 * $this->int_font_size_user, $this->GetStringWidth( $str_text ), $this->int_font_size_user, $str_link );
			}
		}
		if ( $str_output ) {
			$this->Out( $str_output );
		}
		$this->flt_last_cell_height = $flt_height;
		if ( $int_line_number > 0 ) {
			// Go to next line
			$this->flt_position_y += $flt_height;
			if ( $int_line_number == 1 ) {
				$this->flt_position_x = $this->int_left_margin;
			}
		} else {
			$this->flt_position_x += $flt_width;
		}
	}

	/**
	 * Output text with automatic or explicit line breaks
	 *
	 * @param float      $width
	 * @param float      $height
	 * @param string     $text
	 * @param int|string $border Indicates if borders must be drawn around the cell block.
	 * The value can be either a number: 0 - no border, 1 - frame
	 * or a string containing some or all of the following characters (in any order): L - left, 'T' - top, 'R' - right, 'B' - bottom
	 * @param string     $horizontal_alignment
	 * @param string     $vertical_alignment
	 * @param bool       $bol_fill
	 * @param int        $lines_limit
	 *
	 * @return string
	 */
	public function MultiCell( $width, $height, $text, $border = 0, $horizontal_alignment = 'J', $vertical_alignment = 'T', $bol_fill = false, $lines_limit = 0 ) {
		$row_height = 5;

		// was required for calculate char width
		// $arr_character_width = &$this->arr_current_font_info['cw'];

		/* is it really necessary?
		if ( $width == 0 ) {
			$width = $this->flt_current_width - $this->int_right_margin - $this->flt_position_x;
		}
		*/

		// define max width available for text
		$available_width = ( $width - 2 * $this->int_cell_margin );

		// sanitize text
		$text = str_replace( "\r", '', $text );

		// remove last char if it's line break
		if ( $this->bol_uniform_subset ) {
			$int_length = mb_strlen( $text, 'utf-8' );
			while ( $int_length > 0 && mb_substr( $text, $int_length - 1, 1, 'utf-8' ) == "\n" ) {
				$int_length --;
			}
		} else {
			$int_length = strlen( $text );
			if ( $int_length > 0 && $text[ $int_length - 1 ] == "\n" ) {
				$int_length --;
			}
		}

		$adjusted_border      = '';
		$border_between_cells = '';
		if ( $border ) {
			if ( $border === 1 ) {
				$border               = 'LTRB';
				$adjusted_border      = 'LRT';
				$border_between_cells = 'LR';
			} else {
				if ( strpos( $border, 'L' ) !== false ) {
					$border_between_cells .= 'L';
				}
				if ( strpos( $border, 'R' ) !== false ) {
					$border_between_cells .= 'R';
				}

				$adjusted_border = $border_between_cells;
				if ( strpos( $border, 'T' ) !== false ) {
					$adjusted_border = $border_between_cells . 'T';
				}
			}
		}

		$cellCallback = function ( $text, $from, $to, $adjusted_border ) use ( $width, $row_height, $horizontal_alignment, $bol_fill ) {
			if ( $this->bol_uniform_subset ) {
				$this->Cell( $width, $row_height, mb_substr( $text, $from, $to - $from, 'UTF-8' ), $adjusted_border, 2, $horizontal_alignment, $bol_fill );
			} else {
				$this->Cell( $width, $row_height, substr( $text, $from, $to - $from ), $adjusted_border, 2, $horizontal_alignment, $bol_fill );
			}
		};

		$simple_draw_queue = array();

		$last_space_char_pos    = - 1;
		$current_char_pos       = 0;
		$last_line_break_pos    = 0;
		$current_line_width     = 0;
		$space_counter          = 0;
		$line_count             = 1;
		$width_until_last_space = 0;
		while ( $current_char_pos < $int_length ) {
			// Get next character
			if ( $this->bol_uniform_subset ) {
				$current_char = mb_substr( $text, $current_char_pos, 1, 'UTF-8' );
			} else {
				$current_char = $text[ $current_char_pos ];
			}

			if ( $current_char == "\n" ) {
				// Explicit line break
				if ( $this->int_word_spacing > 0 ) {
					$this->int_word_spacing = 0;
					$simple_draw_queue[] = array( array( $this, 'Out' ), array( '0 Tw' ) );
				}

				$simple_draw_queue[] = array(
					$cellCallback,
					array( $text, $last_line_break_pos, $current_char_pos, $adjusted_border )
				);

				$current_char_pos ++;
				$last_space_char_pos = - 1;
				$last_line_break_pos = $current_char_pos;
				$current_line_width  = 0;
				$space_counter       = 0;
				$line_count ++;
				if ( $border && $line_count == 2 ) {
					$adjusted_border = $border_between_cells;
				}
				if ( $lines_limit && $line_count > $lines_limit ) {
					return substr( $text, $current_char_pos );
				}
				continue;
			}

			if ( $current_char == ' ' ) {
				$last_space_char_pos    = $current_char_pos;
				$width_until_last_space = $current_line_width;
				$space_counter ++;
			}

			// GetStringWidth() can calculate depends on $this->bol_uniform_subset
			$current_line_width += $this->GetStringWidth( $current_char );
			/*
			if ( $this->bol_uniform_subset ) {
				$current_line_width += $this->GetStringWidth( $str_character );
			} else {
				$current_line_width += $arr_character_width[ $str_character ] * $this->int_font_size_user / 1000;
			}
			*/

			if ( $current_line_width > $available_width ) {
				// Automatic line break
				if ( $last_space_char_pos == - 1 ) {
					if ( $current_char_pos == $last_line_break_pos ) {
						$current_char_pos ++;
					}
					if ( $this->int_word_spacing > 0 ) {
						$this->int_word_spacing = 0;
						$simple_draw_queue[] = array( array( $this, 'Out' ), array( '0 Tw' ) );
					}

					$simple_draw_queue[] = array(
						$cellCallback,
						array( $text, $last_line_break_pos, $current_char_pos, $adjusted_border )
					);
				} else {
					if ( $horizontal_alignment == 'J' ) {
						$this->int_word_spacing = ( $space_counter > 1 ) ? ( $available_width - $width_until_last_space ) / ( $space_counter - 1 ) : 0;
						$simple_draw_queue[] = array(
							array( $this, 'Out' ),
							array( sprintf( '%.3F Tw', $this->int_word_spacing * $this->flt_scale_factor ) )
						);
					}

					$simple_draw_queue[] = array(
						$cellCallback,
						array( $text, $last_line_break_pos, $last_space_char_pos, $adjusted_border )
					);
					$current_char_pos = $last_space_char_pos + 1;
				}
				$last_space_char_pos = - 1;
				$last_line_break_pos = $current_char_pos;
				$current_line_width  = 0;
				$space_counter       = 0;
				$line_count ++;
				if ( $border && $line_count == 2 ) {
					$adjusted_border = $border_between_cells;
				}

				if ( $lines_limit && $line_count > $lines_limit ) {
					if ( $this->int_word_spacing > 0 ) {
						$this->int_word_spacing = 0;
						$simple_draw_queue[] = array( array( $this, 'Out' ), array( '0 Tw' ) );
					}

					return substr( $text, $current_char_pos );
				}

			} else {
				$current_char_pos ++;
			}
		}

		// Last chunk
		if ( $this->int_word_spacing > 0 ) {
			$this->int_word_spacing = 0;
			$simple_draw_queue[] = array( array( $this, 'Out' ), array( '0 Tw' ) );
		}

		if ( $border && strpos( $border, 'B' ) !== false ) {
			$adjusted_border .= 'B';
		}

		$simple_draw_queue[] = array(
			$cellCallback,
			array( $text, $last_line_break_pos, $current_char_pos, $adjusted_border )
		);

		// install y pos depends on vertical align
		if ( 'C' === $vertical_alignment ) {
			$this->flt_position_y += ( $height - $row_height * $line_count ) / 2;
		} elseif ( 'B' === $vertical_alignment ) {
			$this->flt_position_y += $height - $row_height * $line_count;
		} else {
			// do nothing for top vertical align
		}

		foreach ( $simple_draw_queue as $item ) {
			if ( isset( $item[0] ) && is_callable( $item[0] ) ) {
				$func = $item[0];
				$args = isset( $item[1] ) ? $item[1] : array();

				call_user_func_array( $func, $args );
			}
		}

		$this->flt_position_x = $this->int_left_margin;

		return '';
	}

	/**
	 * @param float  $flt_height
	 * @param string $str_text
	 * @param string $str_link
	 */
	public function Write( $flt_height, $str_text, $str_link = '' ) {
		// Output text in flowing mode
		$arr_character_widths = &$this->arr_current_font_info['cw'];
		$flt_width            = $this->flt_current_width - $this->int_right_margin - $this->flt_position_x;

		$flt_max_width = ( $flt_width - 2 * $this->int_cell_margin );
		$str_text      = str_replace( "\r", '', $str_text );
		if ( $this->bol_uniform_subset ) {
			$int_length = mb_strlen( $str_text, 'UTF-8' );
			if ( $int_length == 1 && $str_text == " " ) {
				$this->flt_position_x += $this->GetStringWidth( $str_text );

				return;
			}
		} else {
			$int_length = strlen( $str_text );
		}
		$int_sep          = - 1;
		$int_i            = 0;
		$int_j            = 0;
		$flt_string_width = 0;
		$int_line_count   = 1;
		while ( $int_i < $int_length ) {
			// Get next character
			if ( $this->bol_uniform_subset ) {
				$str_character = mb_substr( $str_text, $int_i, 1, 'UTF-8' );
			} else {
				$str_character = $str_text[ $int_i ];
			}
			if ( $str_character == "\n" ) {
				// Explicit line break
				if ( $this->bol_uniform_subset ) {
					$this->Cell( $flt_width, $flt_height, mb_substr( $str_text, $int_j, $int_i - $int_j, 'UTF-8' ), 0, 2, '', 0, $str_link );
				} else {
					$this->Cell( $flt_width, $flt_height, substr( $str_text, $int_j, $int_i - $int_j ), 0, 2, '', 0, $str_link );
				}
				$int_i ++;
				$int_sep          = - 1;
				$int_j            = $int_i;
				$flt_string_width = 0;
				if ( $int_line_count == 1 ) {
					$this->flt_position_x = $this->int_left_margin;
					$flt_width            = $this->flt_current_width - $this->int_right_margin - $this->flt_position_x;
					$flt_max_width        = ( $flt_width - 2 * $this->int_cell_margin );
				}
				$int_line_count ++;
				continue;
			}
			if ( $str_character == ' ' ) {
				$int_sep = $int_i;
			}

			if ( $this->bol_uniform_subset ) {
				$flt_string_width += $this->GetStringWidth( $str_character );
			} else {
				$flt_string_width += $arr_character_widths[ $str_character ] * $this->int_font_size_user / 1000;
			}

			if ( $flt_string_width > $flt_max_width ) {
				// Automatic line break
				if ( $int_sep == - 1 ) {
					if ( $this->flt_position_x > $this->int_left_margin ) {
						// Move to next line
						$this->flt_position_x = $this->int_left_margin;
						$this->flt_position_y += $flt_height;
						$flt_width            = $this->flt_current_width - $this->int_right_margin - $this->flt_position_x;
						$flt_max_width        = ( $flt_width - 2 * $this->int_cell_margin );
						$int_i ++;
						$int_line_count ++;
						continue;
					}
					if ( $int_i == $int_j ) {
						$int_i ++;
					}
					if ( $this->bol_uniform_subset ) {
						$this->Cell( $flt_width, $flt_height, mb_substr( $str_text, $int_j, $int_i - $int_j, 'UTF-8' ), 0, 2, '', 0, $str_link );
					} else {
						$this->Cell( $flt_width, $flt_height, substr( $str_text, $int_j, $int_i - $int_j ), 0, 2, '', 0, $str_link );
					}
				} else {
					if ( $this->bol_uniform_subset ) {
						$this->Cell( $flt_width, $flt_height, mb_substr( $str_text, $int_j, $int_sep - $int_j, 'UTF-8' ), 0, 2, '', 0, $str_link );
					} else {
						$this->Cell( $flt_width, $flt_height, substr( $str_text, $int_j, $int_sep - $int_j ), 0, 2, '', 0, $str_link );
					}
					$int_i = $int_sep + 1;
				}
				$int_sep          = - 1;
				$int_j            = $int_i;
				$flt_string_width = 0;
				if ( $int_line_count == 1 ) {
					$this->flt_position_x = $this->int_left_margin;
					$flt_width            = $this->flt_current_width - $this->int_right_margin - $this->flt_position_x;
					$flt_max_width        = ( $flt_width - 2 * $this->int_cell_margin );
				}
				$int_line_count ++;
			} else {
				$int_i ++;
			}
		}
		// Last chunk
		if ( $int_i != $int_j ) {
			if ( $this->bol_uniform_subset ) {
				$this->Cell( $flt_string_width, $flt_height, mb_substr( $str_text, $int_j, $int_i - $int_j, 'UTF-8' ), 0, 0, '', 0, $str_link );
			} else {
				$this->Cell( $flt_string_width, $flt_height, substr( $str_text, $int_j ), 0, 0, '', 0, $str_link );
			}
		}
	}

	/**
	 * @param float|null $flt_height
	 */
	public function Ln( $flt_height = null ) {
		// Line feed; default value is last cell height
		$this->flt_position_x = $this->int_left_margin;
		if ( $flt_height === null ) {
			$this->flt_position_y += $this->flt_last_cell_height;
		} else {
			$this->flt_position_y += $flt_height;
		}
	}

	/**
	 * @param            $str_file
	 * @param float|null $flt_x
	 * @param float|null $flt_y
	 * @param int        $int_width
	 * @param int        $int_height
	 * @param string     $str_type
	 * @param string     $str_link
	 */
	public function Image( $str_file, $flt_x = null, $flt_y = null, $int_width = 0, $int_height = 0, $str_type = '', $str_link = '' ) {
		// Put an image on the page
		if ( ! isset( $this->arr_images[ $str_file ] ) ) {
			// First use of this image, get info
			if ( $str_type == '' ) {
				$int_position = strrpos( $str_file, '.' );
				if ( ! $int_position ) {
					throw new WOE_FPDF_Exception( esc_html('Image file has no extension and no type was specified: ' . $str_file), esc_html(WOE_FPDF_Exception::INVALID_IMAGE) );
				}
				$str_type = substr( $str_file, $int_position + 1 );
			}
			$str_type = strtolower( $str_type );
			if ( $str_type == 'jpeg' ) {
				$str_type = 'jpg';
			}
			$str_method = '_parse' . $str_type;
			if ( ! method_exists( $this, $str_method ) ) {
				throw new WOE_FPDF_Exception( esc_html('Unsupported image type: `' . $str_type . '`'), esc_html(WOE_FPDF_Exception::UNSUPPORTED_IMAGE) );
			}
			$arr_image_info                = $this->$str_method( $str_file );
			$arr_image_info['i']           = count( $this->arr_images ) + 1;
			$this->arr_images[ $str_file ] = $arr_image_info;
		} else {
			$arr_image_info = $this->arr_images[ $str_file ];
		}

		// Automatic width and height calculation if needed
		if ( $int_width == 0 && $int_height == 0 ) {
			// Put image at 96 dpi
			$int_width  = - 96;
			$int_height = - 96;
		}
		if ( $int_width < 0 ) {
			$int_width = - $arr_image_info['w'] * 72 / $int_width / $this->flt_scale_factor;
		}
		if ( $int_height < 0 ) {
			$int_height = - $arr_image_info['h'] * 72 / $int_height / $this->flt_scale_factor;
		}
		if ( $int_width == 0 ) {
			$int_width = $int_height * $arr_image_info['w'] / $arr_image_info['h'];
		}
		if ( $int_height == 0 ) {
			$int_height = $int_width * $arr_image_info['h'] / $arr_image_info['w'];
		}

		// Flowing mode
		if ( $flt_y === null ) {
			if ( $this->flt_position_y + $int_height > $this->flt_page_break_trigger && ! $this->bol_in_header && ! $this->bol_in_footer && $this->AcceptPageBreak() ) {
				// Automatic page break
				$flt_new_x_pos = $this->flt_position_x;
				$this->AddPage( $this->str_current_orientation, $this->arr_current_page_sizes );
				$this->flt_position_x = $flt_new_x_pos;
			}
			$flt_y                = $this->flt_position_y;
			$this->flt_position_y += $int_height;
		}

		if ( $flt_x === null ) {
			$flt_x = $this->flt_position_x;
		}
		$this->Out( sprintf( 'q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q', $int_width * $this->flt_scale_factor, $int_height * $this->flt_scale_factor, $flt_x * $this->flt_scale_factor, ( $this->flt_current_height - ( $flt_y + $int_height ) ) * $this->flt_scale_factor, $arr_image_info['i'] ) );
		if ( $str_link ) {
			$this->Link( $flt_x, $flt_y, $int_width, $int_height, $str_link );
		}
	}

	/**
	 * @return mixed
	 */
	public function GetX() {
		// Get x position
		return $this->flt_position_x;
	}

	/**
	 * @param $flt_position_x
	 */
	public function SetX( $flt_position_x ) {
		// Set x position
		if ( $flt_position_x >= 0 ) {
			$this->flt_position_x = $flt_position_x;
		} else {
			$this->flt_position_x = $this->flt_current_width + $flt_position_x;
		}
	}

	/**
	 * @return float
	 */
	public function GetY() {
		// Get y position
		return $this->flt_position_y;
	}

	/**
	 * @param $flt_position_y
	 */
	public function SetY( $flt_position_y ) {
		// Set y position and reset x
		$this->flt_position_x = $this->int_left_margin;
		if ( $flt_position_y >= 0 ) {
			$this->flt_position_y = $flt_position_y;
		} else {
			$this->flt_position_y = $this->flt_current_height + $flt_position_y;
		}
	}

	/**
	 * @param $x
	 * @param $y
	 */
	public function SetXY( $x, $y ) {
		// Set x and y positions
		$this->SetY( $y );
		$this->SetX( $x );
	}

	/**
	 * @return string
	 */
	public function output() {
		// Output PDF to some destination
		if ( $this->int_state < self::DOCUMENT_STATE_TERMINATED ) {
			$this->Close();
		}

		return $this->str_buffer;
	}

	/**
	 * Set the font directory where font should be loaded front
	 *
	 * @param $fontPath string The font directory
	 *
	 * @return string
	 */
	public function setFontPath( $fontPath ) {
		if ( ! file_exists( $fontPath ) || ! is_dir( $fontPath ) ) {
			throw new WOE_FPDF_Exception( esc_html('Font path does not exist `' . $fontPath . '`'), esc_html(WOE_FPDF_Exception::INVALID_FONT_PATH) );
		}

		$this->str_font_path = realpath( $fontPath ) . '/';

		return $this->str_font_path;
	}

	/**
	 * @return string
	 */
	protected function getFontPath() {
		return realpath( $this->str_font_path ) . '/';
	}

	/**
	 *
	 */
	protected function checkOutput() {
		if ( PHP_SAPI !== 'cli' ) {
			if ( headers_sent( $str_file, $int_line ) ) {
				throw new WOE_FPDF_Exception( esc_html("Some data has already been output, can't send PDF file, output started at " . $str_file . ":" . $int_line), esc_html(WOE_FPDF_Exception::HEADER_ALREADY_SENT) );
			}
		}

		if ( ob_get_length() ) {

			// The output buffer is not empty
			if ( ! preg_match( '/^(\xEF\xBB\xBF)?\s*$/', ob_get_contents() ) ) {
				throw new WOE_FPDF_Exception( esc_html('Some data has already been output, can\'t send PDF file'), esc_html(WOE_FPDF_Exception::HEADER_ALREADY_SENT) );
			}

			// It contains only a UTF-8 BOM and/or whitespace, let's clean it
			ob_clean();
		}
	}

	/**
	 * @param $mix_size
	 *
	 * @return array|string
	 */
	private function getPageSize( $mix_size ) {
		if ( is_string( $mix_size ) ) {
			$mix_size = strtolower( $mix_size );
			if ( ! isset( $this->arr_standard_page_sizes[ $mix_size ] ) ) {
				throw new WOE_FPDF_Exception( esc_html('Invalid page size: ' . $mix_size), esc_html(WOE_FPDF_Exception::INVALID_PAGE_SIZE) );
			}
			$a = $this->arr_standard_page_sizes[ $mix_size ];

			return array( $a[0] / $this->flt_scale_factor, $a[1] / $this->flt_scale_factor );
		} else {
			if ( $mix_size[0] > $mix_size[1] ) {
				return array( $mix_size[1], $mix_size[0] );
			} else {
				return $mix_size;
			}
		}
	}

	/**
	 * @param $str_orientation
	 * @param $mix_size
	 */
	private function BeginPage( $str_orientation, $mix_size ) {
		$this->int_page ++;
		$this->arr_pages[ $this->int_page ] = '';
		$this->int_state                    = self::DOCUMENT_STATE_CREATING;
		$this->flt_position_x               = $this->int_left_margin;
		$this->flt_position_y               = $this->int_top_margin;
		$this->str_current_font_family      = '';
		// Check page size and orientation
		if ( $str_orientation == '' ) {
			$str_orientation = $this->str_default_orientation;
		} else {
			$str_orientation = strtoupper( $str_orientation[0] );
		}
		if ( $mix_size == '' ) {
			$mix_size = $this->arr_default_page_sizes;
		} else {
			$mix_size = $this->getPageSize( $mix_size );
		}
		if ( $str_orientation != $this->str_current_orientation || $mix_size[0] != $this->arr_current_page_sizes[0] || $mix_size[1] != $this->arr_current_page_sizes[1] ) {
			// New size or orientation
			if ( $str_orientation == self::ORIENTATION_PORTRAIT ) {
				$this->flt_current_width  = $mix_size[0];
				$this->flt_current_height = $mix_size[1];
			} else { // landscape
				$this->flt_current_width  = $mix_size[1];
				$this->flt_current_height = $mix_size[0];
			}
			$this->flt_width_points        = $this->flt_current_width * $this->flt_scale_factor;
			$this->flt_height_points       = $this->flt_current_height * $this->flt_scale_factor;
			$this->flt_page_break_trigger  = $this->flt_current_height - $this->int_break_margin;
			$this->str_current_orientation = $str_orientation;
			$this->arr_current_page_sizes  = $mix_size;
		}
		if ( $str_orientation != $this->str_default_orientation || $mix_size[0] != $this->arr_default_page_sizes[0] || $mix_size[1] != $this->arr_default_page_sizes[1] ) {
			$this->arr_page_sizes[ $this->int_page ] = array( $this->flt_width_points, $this->flt_height_points );
		}
	}

	/**
	 *
	 */
	private function EndPage() {
		$this->int_state = self::DOCUMENT_STATE_INITIALIZED;
	}

	/**
	 * @param $str_font
	 *
	 * @return array
	 */
	private function LoadFont( $str_font ) {
		// Load a font definition file from the font directory
		include( $this->str_font_path . $str_font );
		$arr_defined_vars = get_defined_vars();
		if ( ! isset( $arr_defined_vars['name'] ) ) {
			throw new WOE_FPDF_Exception( esc_html('Could not include font definition file'), esc_html(WOE_FPDF_Exception::INVALID_FONT_FILE) );
		}

		return $arr_defined_vars;
	}

	/**
	 * @param $str_text
	 *
	 * @return mixed
	 */
	protected function EscapeString( $str_text ) {
		// Escape special characters in strings
		$str_text = str_replace( '\\', '\\\\', $str_text );
		$str_text = str_replace( '(', '\\(', $str_text );
		$str_text = str_replace( ')', '\\)', $str_text );
		$str_text = str_replace( "\r", '\\r', $str_text );

		return $str_text;
	}

	/**
	 * @param $s
	 *
	 * @return string
	 */
	protected function TextString( $s ) {
		// Format a text string
		return '(' . $this->EscapeString( $s ) . ')';
	}

	/**
	 * @param $str_text
	 *
	 * @return string
	 */
	private function UTF8toUTF16( $str_text ) {
		// Convert UTF-8 to UTF-16BE with BOM
		$str_res    = "\xFE\xFF";
		$int_length = strlen( $str_text );
		$i          = 0;
		while ( $i < $int_length ) {
			$int_character_1 = ord( $str_text[ $i ++ ] );
			if ( $int_character_1 >= 224 ) {
				// 3-byte character
				$int_character_2 = ord( $str_text[ $i ++ ] );
				$int_character_3 = ord( $str_text[ $i ++ ] );
				$str_res         .= chr( ( ( $int_character_1 & 0x0F ) << 4 ) + ( ( $int_character_2 & 0x3C ) >> 2 ) );
				$str_res         .= chr( ( ( $int_character_2 & 0x03 ) << 6 ) + ( $int_character_3 & 0x3F ) );
			} elseif ( $int_character_1 >= 192 ) {
				// 2-byte character
				$int_character_2 = ord( $str_text[ $i ++ ] );
				$str_res         .= chr( ( $int_character_1 & 0x1C ) >> 2 );
				$str_res         .= chr( ( ( $int_character_1 & 0x03 ) << 6 ) + ( $int_character_2 & 0x3F ) );
			} else {
				// Single-byte character
				$str_res .= "\0" . chr( $int_character_1 );
			}
		}

		return $str_res;
	}

	private function DoUnderline( $flt_x, $flt_y, $str_text ) {
		// Underline text
		$flt_underline_position  = $this->arr_current_font_info['up'];
		$flt_underline_thickness = $this->arr_current_font_info['ut'];
		$flt_width               = $this->GetStringWidth( $str_text ) + $this->int_word_spacing * substr_count( $str_text, ' ' );

		return sprintf( '%.2F %.2F %.2F %.2F re f', $flt_x * $this->flt_scale_factor, ( $this->flt_current_height - ( $flt_y - $flt_underline_position / 1000 * $this->int_font_size_user ) ) * $this->flt_scale_factor, $flt_width * $this->flt_scale_factor, - $flt_underline_thickness / 1000 * $this->int_current_font_size );
	}

	/**
	 * @param $str_file
	 *
	 * @return array
	 */
	private function _parsejpg( $str_file ) {
		// Extract info from a JPEG file
		$arr_size_data = getimagesize( $str_file );
		if ( ! $arr_size_data ) {
			throw new WOE_FPDF_Exception( esc_html('Missing or incorrect image file: ' . $str_file), esc_html(WOE_FPDF_Exception::INVALID_IMAGE) );
		}
		if ( $arr_size_data[2] != 2 ) {
			throw new WOE_FPDF_Exception( 'Not a JPEG file: ' . esc_html($str_file), esc_html(WOE_FPDF_Exception::UNSUPPORTED_IMAGE) );
		}
		if ( ! isset( $arr_size_data['channels'] ) || $arr_size_data['channels'] == 3 ) {
			$str_color_space = 'DeviceRGB';
		} elseif ( $arr_size_data['channels'] == 4 ) {
			$str_color_space = 'DeviceCMYK';
		} else {
			$str_color_space = 'DeviceGray';
		}
		$int_bits_per_component = isset( $arr_size_data['bits'] ) ? $arr_size_data['bits'] : 8;
		$str_data               = file_get_contents( $str_file );

		return array(
			'w'    => $arr_size_data[0],
			'h'    => $arr_size_data[1],
			'cs'   => $str_color_space,
			'bpc'  => $int_bits_per_component,
			'f'    => 'DCTDecode',
			'data' => $str_data
		);
	}

    /**
     * @param $str_file
     *
     * @return array
     */
    private function _parsewebp($str_file)
    {
        $objImage = imagecreatefromwebp($str_file);

        $strTmp = tempnam('.', 'png');
        if (!$strTmp) {
            throw new WOE_FPDF_Exception( esc_html('Unable to create a temporary file'), esc_html(WOE_FPDF_Exception::IMAGE_NOT_WRITABLE));
        }
        if (!imagepng($objImage, $strTmp)) {
            throw new WOE_FPDF_Exception( esc_html('Error while saving to temporary file'), esc_html(WOE_FPDF_Exception::IMAGE_NOT_WRITABLE));
        }
        if (PHP_MAJOR_VERSION < 8)
			imagedestroy($objImage);
        $arrInfo = $this->_parsepng($strTmp);
        wp_delete_file($strTmp);

        return $arrInfo;
    }

    private function _parseavif($str_file)
    {
        $arrInfo = array();

        if (function_exists('imagecreatefromavif')) {
            $objImage = imagecreatefromavif($str_file);

            $strTmp = tempnam('.', 'png');
            if (!$strTmp) {
                throw new WOE_FPDF_Exception( esc_html('Unable to create a temporary file'), esc_html(WOE_FPDF_Exception::IMAGE_NOT_WRITABLE) );
            }
            if (!imagepng($objImage, $strTmp)) {
                throw new WOE_FPDF_Exception( esc_html('Error while saving to temporary file'), esc_html(WOE_FPDF_Exception::IMAGE_NOT_WRITABLE) );
            }
            if (PHP_MAJOR_VERSION < 8)
				imagedestroy($objImage);
            $arrInfo = $this->_parsepng($strTmp);
            wp_delete_file($strTmp);
        }

        return $arrInfo;
    }

	/**
	 * @param $str_file
	 *
	 * @return array
	 */
	private function _parsepng( $str_file ) {
		// Extract info from a PNG file
		$ptr_file = fopen( $str_file, 'rb' );
		if ( ! $ptr_file ) {
			throw new WOE_FPDF_Exception( esc_html('Can\'t open image file: ' . $str_file), esc_html(WOE_FPDF_Exception::INVALID_IMAGE) );
		}
		$arr_info = $this->_parsepngstream( $ptr_file, $str_file );
		fclose( $ptr_file );

		return $arr_info;
	}

	/**
	 * @param $ptr_file
	 * @param $str_file
	 *
	 * @return array
	 */
	private function _parsepngstream( $ptr_file, $str_file ) {
		// Check signature
		if ( $this->_readstream( $ptr_file, 8 ) != chr( 137 ) . 'PNG' . chr( 13 ) . chr( 10 ) . chr( 26 ) . chr( 10 ) ) {
			throw new WOE_FPDF_Exception( esc_html('Not a PNG file: ' . $str_file), esc_html(WOE_FPDF_Exception::UNSUPPORTED_IMAGE) );
		}

		// Read header chunk
		$this->_readstream( $ptr_file, 4 );
		if ( $this->_readstream( $ptr_file, 4 ) != 'IHDR' ) {
			throw new WOE_FPDF_Exception( esc_html('Incorrect PNG file: ' . $str_file), esc_html(WOE_FPDF_Exception::UNSUPPORTED_IMAGE) );
		}
		$int_width              = $this->_readint( $ptr_file );
		$int_height             = $this->_readint( $ptr_file );
		$int_bits_per_component = ord( $this->_readstream( $ptr_file, 1 ) );
		if ( $int_bits_per_component > 8 ) {
			throw new WOE_FPDF_Exception( esc_html('16-bit depth not supported: ' . $str_file), esc_html(WOE_FPDF_Exception::UNSUPPORTED_IMAGE) );
		}
		$int_color_channels = ord( $this->_readstream( $ptr_file, 1 ) );
		if ( $int_color_channels == 0 || $int_color_channels == 4 ) {
			$str_color_space = 'DeviceGray';
		} elseif ( $int_color_channels == 2 || $int_color_channels == 6 ) {
			$str_color_space = 'DeviceRGB';
		} elseif ( $int_color_channels == 3 ) {
			$str_color_space = 'Indexed';
		} else {
			throw new WOE_FPDF_Exception( 'Unknown color type: ' . esc_html($str_file), esc_html(WOE_FPDF_Exception::UNSUPPORTED_IMAGE) );
		}
		if ( ord( $this->_readstream( $ptr_file, 1 ) ) != 0 ) {
			throw new WOE_FPDF_Exception( 'Unknown compression method: ' . esc_html($str_file), esc_html(WOE_FPDF_Exception::UNSUPPORTED_IMAGE) );
		}
		if ( ord( $this->_readstream( $ptr_file, 1 ) ) != 0 ) {
			throw new WOE_FPDF_Exception( 'Unknown filter method: ' . esc_html($str_file), esc_html(WOE_FPDF_Exception::UNSUPPORTED_IMAGE) );
		}
		if ( ord( $this->_readstream( $ptr_file, 1 ) ) != 0 ) {
			throw new WOE_FPDF_Exception( 'Interlacing not supported: ' . esc_html($str_file), esc_html(WOE_FPDF_Exception::UNSUPPORTED_IMAGE) );
		}
		$this->_readstream( $ptr_file, 4 );
		$str_predictor = '/Predictor 15 /Colors ' . ( $str_color_space == 'DeviceRGB' ? 3 : 1 ) . ' /BitsPerComponent ' . $int_bits_per_component . ' /Columns ' . $int_width;

		// Scan chunks looking for palette, transparency and image data
		$str_palette           = '';
		$arr_transparency_data = '';
		$str_data              = '';
		do {
			$int_line = $this->_readint( $ptr_file );
			$str_type = $this->_readstream( $ptr_file, 4 );
			if ( $str_type == 'PLTE' ) {
				// Read palette
				$str_palette = $this->_readstream( $ptr_file, $int_line );
				$this->_readstream( $ptr_file, 4 );
			} elseif ( $str_type == 'tRNS' ) {
				// Read transparency info
				$str_transparency = $this->_readstream( $ptr_file, $int_line );
				if ( $int_color_channels == 0 ) {
					$arr_transparency_data = array( ord( substr( $str_transparency, 1, 1 ) ) );
				} elseif ( $int_color_channels == 2 ) {
					$arr_transparency_data = array(
						ord( substr( $str_transparency, 1, 1 ) ),
						ord( substr( $str_transparency, 3, 1 ) ),
						ord( substr( $str_transparency, 5, 1 ) )
					);
				} else {
					$int_pos = strpos( $str_transparency, chr( 0 ) );
					if ( $int_pos !== false ) {
						$arr_transparency_data = array( $int_pos );
					}
				}
				$this->_readstream( $ptr_file, 4 );
			} elseif ( $str_type == 'IDAT' ) {
				// Read image data block
				$str_data .= $this->_readstream( $ptr_file, $int_line );
				$this->_readstream( $ptr_file, 4 );
			} elseif ( $str_type == 'IEND' ) {
				break;
			} else {
				$this->_readstream( $ptr_file, $int_line + 4 );
			}
		} while ( $int_line );

		if ( $str_color_space == 'Indexed' && empty( $str_palette ) ) {
			throw new WOE_FPDF_Exception( 'Missing palette in ' . esc_html($str_file), esc_html(WOE_FPDF_Exception::UNSUPPORTED_IMAGE) );
		}
		$arr_info = array(
			'w'    => $int_width,
			'h'    => $int_height,
			'cs'   => $str_color_space,
			'bpc'  => $int_bits_per_component,
			'f'    => 'FlateDecode',
			'dp'   => $str_predictor,
			'pal'  => $str_palette,
			'trns' => $arr_transparency_data
		);
		if ( $int_color_channels >= 4 ) {
			// Extract alpha channel
			if ( ! function_exists( 'gzuncompress' ) ) {
				throw new WOE_FPDF_Exception( 'Zlib not available, can\'t handle alpha channel: ' . esc_html($str_file), esc_html(WOE_FPDF_Exception::EXTENSION_NOT_AVAILABLE) );
			}
			$str_data  = gzuncompress( $str_data );
			$str_color = '';
			$str_alpha = '';
			if ( $int_color_channels == 4 ) {
				// Gray image
				$int_length = 2 * $int_width;
				for ( $i = 0; $i < $int_height; $i ++ ) {
					$int_pos   = ( 1 + $int_length ) * $i;
					$str_color .= $str_data[ $int_pos ];
					$str_alpha .= $str_data[ $int_pos ];
					$str_line  = substr( $str_data, $int_pos + 1, $int_length );
					$str_color .= preg_replace( '/(.)./s', '$1', $str_line );
					$str_alpha .= preg_replace( '/.(.)/s', '$1', $str_line );
				}
			} else {
				// RGB image
				$int_length = 4 * $int_width;
				for ( $i = 0; $i < $int_height; $i ++ ) {
					$int_pos   = ( 1 + $int_length ) * $i;
					$str_color .= $str_data[ $int_pos ];
					$str_alpha .= $str_data[ $int_pos ];
					$str_line  = substr( $str_data, $int_pos + 1, $int_length );
					$str_color .= preg_replace( '/(.{3})./s', '$1', $str_line );
					$str_alpha .= preg_replace( '/.{3}(.)/s', '$1', $str_line );
				}
			}
			unset( $str_data );
			$str_data          = gzcompress( $str_color );
			$arr_info['smask'] = gzcompress( $str_alpha );
			if ( $this->str_pdf_version < '1.4' ) {
				$this->str_pdf_version = '1.4';
			}
		}
		$arr_info['data'] = $str_data;

		return $arr_info;
	}

	/**
	 * @param $ptr_file
	 * @param $int_bytes
	 *
	 * @return string
	 */
	private function _readstream( $ptr_file, $int_bytes ) {
		// Read n bytes from stream
		$str_result = '';
		while ( $int_bytes > 0 && ! feof( $ptr_file ) ) {
			$str_data = fread( $ptr_file, $int_bytes );
			if ( $str_data === false ) {
				throw new WOE_FPDF_Exception( 'Error while reading stream', esc_html(WOE_FPDF_Exception::INVALID_STREAM) );
			}
			$int_bytes  -= strlen( $str_data );
			$str_result .= $str_data;
		}
		if ( $int_bytes > 0 ) {
			throw new WOE_FPDF_Exception( 'Unexpected end of stream', esc_html(WOE_FPDF_Exception::INVALID_STREAM) );
		}

		return $str_result;
	}

	/**
	 * @param $ptr_file
	 *
	 * @return mixed
	 */
	private function _readint( $ptr_file ) {
		// Read a 4-byte integer from stream
		$arr_data = unpack( 'Ni', $this->_readstream( $ptr_file, 4 ) );

		return $arr_data['i'];
	}

	/**
	 * @param $str_file
	 *
	 * @return array
	 */
	private function _parsegif( $str_file ) {
		// Extract info from a GIF file (via PNG conversion)
		if ( ! function_exists( 'imagepng' ) ) {
			throw new WOE_FPDF_Exception( 'GD extension is required for GIF support', esc_html(WOE_FPDF_Exception::EXTENSION_NOT_AVAILABLE) );
		}
		if ( ! function_exists( 'imagecreatefromgif' ) ) {
			throw new WOE_FPDF_Exception( 'GD has no GIF read support', esc_html(WOE_FPDF_Exception::EXTENSION_NOT_AVAILABLE) );
		}
		$obj_image = imagecreatefromgif( $str_file );
		if ( ! $obj_image ) {
			throw new WOE_FPDF_Exception( 'Missing or incorrect image file: ' . esc_html($str_file), esc_html(WOE_FPDF_Exception::INVALID_IMAGE) );
		}
		imageinterlace( $obj_image, 0 );
		$ptr_file = @fopen( 'php://temp', 'rb+' );
		if ( $ptr_file ) {
			// Perform conversion in memory
			ob_start();
			imagepng( $obj_image );
			$str_data = ob_get_clean();
			if (PHP_MAJOR_VERSION < 8)
				imagedestroy( $obj_image );
			fwrite( $ptr_file, $str_data );
			rewind( $ptr_file );
			$arr_info = $this->_parsepngstream( $ptr_file, $str_file );
			fclose( $ptr_file );
		} else {
			// Use temporary file
			$str_tmp = tempnam( '.', 'gif' );
			if ( ! $str_tmp ) {
				throw new WOE_FPDF_Exception( 'Unable to create a temporary file', esc_html(WOE_FPDF_Exception::IMAGE_NOT_WRITABLE) );
			}
			if ( ! imagepng( $obj_image, $str_tmp ) ) {
				throw new WOE_FPDF_Exception( 'Error while saving to temporary file', esc_html(WOE_FPDF_Exception::IMAGE_NOT_WRITABLE) );
			}
			if (PHP_MAJOR_VERSION < 8)
				imagedestroy( $obj_image );
			$arr_info = $this->_parsepng( $str_tmp );
			wp_delete_file( $str_tmp );
		}

		return $arr_info;
	}

	/**
	 * Creates a new object
	 */
	protected function NewObject() {
		$this->int_current_object ++;
		$this->arr_offsets[ $this->int_current_object ] = strlen( $this->str_buffer );
		$this->Out( $this->int_current_object . ' 0 obj' );
	}

	/**
	 * @param $str_data
	 */
	protected function PutStream( $str_data ) {
		$this->Out( 'stream' );
		$this->Out( $str_data );
		$this->Out( 'endstream' );
	}

	/**
	 * @param $str_data
	 */
	protected function Out( $str_data ) {
		// Add a line to the document
		if ( $this->int_state == self::DOCUMENT_STATE_CREATING ) {
			$this->arr_pages[ $this->int_page ] .= $str_data . "\n";
		} else {
			$this->str_buffer .= $str_data . "\n";
		}
	}

	/**
	 * Puts the pages into the document
	 */
	private function PutPages() {
		$int_page = $this->int_page;
		if ( ! empty( $this->str_alias_number_pages ) ) {
			// Replace number of pages in fonts using subsets
			$str_alias   = $this->UTF8ToUTF16BE( $this->str_alias_number_pages, false );
			$str_replace = $this->UTF8ToUTF16BE( "$int_page", false );
			for ( $n = 1; $n <= $int_page; $n ++ ) {
				$this->arr_pages[ $n ] = str_replace( $str_alias, $str_replace, $this->arr_pages[ $n ] );
			}
			// Now repeat for no pages in non-subset fonts
			for ( $n = 1; $n <= $int_page; $n ++ ) {
				$this->arr_pages[ $n ] = str_replace( $this->str_alias_number_pages, $int_page, $this->arr_pages[ $n ] );
			}
		}
		if ( $this->str_default_orientation == self::ORIENTATION_PORTRAIT ) {
			$flt_page_width  = $this->arr_default_page_sizes[0] * $this->flt_scale_factor;
			$flt_page_height = $this->arr_default_page_sizes[1] * $this->flt_scale_factor;
		} else {
			$flt_page_width  = $this->arr_default_page_sizes[1] * $this->flt_scale_factor;
			$flt_page_height = $this->arr_default_page_sizes[0] * $this->flt_scale_factor;
		}
		$str_filter = ( $this->bol_compress ) ? '/Filter /FlateDecode ' : '';
		for ( $n = 1; $n <= $int_page; $n ++ ) {
			// Page
			$this->NewObject();
			$this->Out( '<</Type /Page' );
			$this->Out( '/Parent 1 0 R' );
			if ( isset( $this->arr_page_sizes[ $n ] ) ) {
				$this->Out( sprintf( '/MediaBox [0 0 %.2F %.2F]', $this->arr_page_sizes[ $n ][0], $this->arr_page_sizes[ $n ][1] ) );
			}
			$this->Out( '/Resources 2 0 R' );
			if ( isset( $this->arr_page_links[ $n ] ) ) {
				// Links
				$str_annotations = '/Annots [';
				foreach ( $this->arr_page_links[ $n ] as $arr_page_link ) {
					$str_rectangle   = sprintf( '%.2F %.2F %.2F %.2F', $arr_page_link[0], $arr_page_link[1], $arr_page_link[0] + $arr_page_link[2], $arr_page_link[1] - $arr_page_link[3] );
					$str_annotations .= '<</Type /Annot /Subtype /Link /Rect [' . $str_rectangle . '] /Border [0 0 0] ';
					if ( is_string( $arr_page_link[4] ) ) {
						$str_annotations .= '/A <</S /URI /URI ' . $this->TextString( $arr_page_link[4] ) . '>>>>';
					} else {
						$arr_link        = $this->arr_internal_links[ $arr_page_link[4] ];
						$flt_height      = isset( $this->arr_page_sizes[ $arr_link[0] ] ) ? $this->arr_page_sizes[ $arr_link[0] ][1] : $flt_page_height;
						$str_annotations .= sprintf( '/Dest [%d 0 R /XYZ 0 %.2F null]>>', 1 + 2 * $arr_link[0], $flt_height - $arr_link[1] * $this->flt_scale_factor );
					}
				}
				$this->Out( $str_annotations . ']' );
			}
			if ( $this->str_pdf_version > '1.3' ) {
				$this->Out( '/Group <</Type /Group /S /Transparency /CS /DeviceRGB>>' );
			}
			$this->Out( '/Contents ' . ( $this->int_current_object + 1 ) . ' 0 R>>' );
			$this->Out( 'endobj' );
			// Page content
			$str_page_content = ( $this->bol_compress ) ? gzcompress( $this->arr_pages[ $n ] ) : $this->arr_pages[ $n ];
			$this->NewObject();
			$this->Out( '<<' . $str_filter . '/Length ' . strlen( $str_page_content ) . '>>' );
			$this->PutStream( $str_page_content );
			$this->Out( 'endobj' );
		}
		// Pages root
		$this->arr_offsets[1] = strlen( $this->str_buffer );
		$this->Out( '1 0 obj' );
		$this->Out( '<</Type /Pages' );
		$str_kids = '/Kids [';
		for ( $i = 0; $i < $int_page; $i ++ ) {
			$str_kids .= ( 3 + 2 * $i ) . ' 0 R ';
		}
		$this->Out( $str_kids . ']' );
		$this->Out( '/Count ' . $int_page );
		$this->Out( sprintf( '/MediaBox [0 0 %.2F %.2F]', $flt_page_width, $flt_page_height ) );
		$this->Out( '>>' );
		$this->Out( 'endobj' );
	}

	/**
	 *
	 */
	public function PutFonts() {
		$int_current_object = $this->int_current_object;
		foreach ( $this->arr_encoding_diffs as $str_diff ) {
			// Encodings
			$this->NewObject();
			$this->Out( '<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences [' . $str_diff . ']>>' );
			$this->Out( 'endobj' );
		}
		foreach ( $this->arr_font_files as $str_file => $arr_info ) {
			if ( ! isset( $arr_info['type'] ) || $arr_info['type'] != self::FONT_TRUETYPE ) {
				// Font file embedding
				$this->NewObject();
				$this->arr_font_files[ $str_file ]['n'] = $this->int_current_object;
				$str_font                               = $this->readFontFile( $this->getFontPath() . $str_file );

				$bol_compressed_file = ( substr( $str_file, - 2 ) == '.z' );
				if ( ! $bol_compressed_file && isset( $arr_info['length2'] ) ) {
					$bol_header = ( ord( $str_font[0] ) == 128 );
					if ( $bol_header ) {
						// Strip first binary header
						$str_font = substr( $str_font, 6 );
					}
					if ( $bol_header && ord( $str_font[ $arr_info['length1'] ] ) == 128 ) {
						// Strip second binary header
						$str_font = substr( $str_font, 0, $arr_info['length1'] ) . substr( $str_font, $arr_info['length1'] + 6 );
					}
				}
				$this->Out( '<</Length ' . strlen( $str_font ) );
				if ( $bol_compressed_file ) {
					$this->Out( '/Filter /FlateDecode' );
				}
				$this->Out( '/Length1 ' . $arr_info['length1'] );
				if ( isset( $arr_info['length2'] ) ) {
					$this->Out( '/Length2 ' . $arr_info['length2'] . ' /Length3 0' );
				}
				$this->Out( '>>' );
				$this->PutStream( $str_font );
				$this->Out( 'endobj' );
			}
		}

		foreach ( $this->arr_fonts as $str_key => $arr_font_data ) {

			// Font objects
			//$this->fonts[$k]['n']=$this->n+1;
			$str_type = $arr_font_data['type'];
			$str_name = $arr_font_data['name'];
			if ( $str_type == 'Core' ) {
				// Standard font
				$this->arr_fonts[ $str_key ]['n'] = $this->int_current_object + 1;
				$this->NewObject();
				$this->Out( '<</Type /Font' );
				$this->Out( '/BaseFont /' . $str_name );
				$this->Out( '/Subtype /Type1' );
				if ( $str_name != 'Symbol' && $str_name != 'ZapfDingbats' ) {
					$this->Out( '/Encoding /WinAnsiEncoding' );
				}
				$this->Out( '>>' );
				$this->Out( 'endobj' );
			} elseif ( $str_type == 'Type1' || $str_type == 'TrueType' ) {
				// Additional Type1 or TrueType font
				$this->arr_fonts[ $str_key ]['n'] = $this->int_current_object + 1;
				$this->NewObject();
				$this->Out( '<</Type /Font' );
				$this->Out( '/BaseFont /' . $str_name );
				$this->Out( '/Subtype /' . $str_type );
				$this->Out( '/FirstChar 32 /LastChar 255' );
				$this->Out( '/Widths ' . ( $this->int_current_object + 1 ) . ' 0 R' );
				$this->Out( '/FontDescriptor ' . ( $this->int_current_object + 2 ) . ' 0 R' );
				if ( $arr_font_data['enc'] ) {
					if ( isset( $arr_font_data['diff'] ) ) {
						$this->Out( '/Encoding ' . ( $int_current_object + $arr_font_data['diff'] ) . ' 0 R' );
					} else {
						$this->Out( '/Encoding /WinAnsiEncoding' );
					}
				}
				$this->Out( '>>' );
				$this->Out( 'endobj' );
				// Widths
				$this->NewObject();
				$int_character_width =& $arr_font_data['cw'];
				$str_data            = '[';
				for ( $i = 32; $i <= 255; $i ++ ) {
					$str_data .= $int_character_width[ chr( $i ) ] . ' ';
				}
				$this->Out( $str_data . ']' );
				$this->Out( 'endobj' );
				// Descriptor
				$this->NewObject();
				$str_data = '<</Type /FontDescriptor /FontName /' . $str_name;
				foreach ( $arr_font_data['desc'] as $str_inner_key => $str_value ) {
					$str_data .= ' /' . $str_inner_key . ' ' . $str_value;
				}
				$str_file = $arr_font_data['file'];
				if ( $str_file ) {
					$str_data .= ' /FontFile' . ( $str_type == 'Type1' ? '' : '2' ) . ' ' . $this->arr_font_files[ $str_file ]['n'] . ' 0 R';
				}
				$this->Out( $str_data . '>>' );
				$this->Out( 'endobj' );
			} // TrueType embedded SUBSETS or FULL
			else {
				if ( $str_type === self::FONT_TRUETYPE ) {
					$this->arr_fonts[ $str_key ]['n'] = $this->int_current_object + 1;

					$obj_ttf       = new WOE_FPDF_TT_Font_File();
					$str_font_name = 'MPDFAA' . '+' . $arr_font_data['name'];
					$str_subset    = $arr_font_data['subset'];
					unset( $str_subset[0] );
					$str_ttf_font_stream        = $obj_ttf->makeSubset( $arr_font_data['ttffile'], $str_subset );
					$int_ttf_font_size          = strlen( $str_ttf_font_stream );
					$str_font_stream_compressed = gzcompress( $str_ttf_font_stream );
					$str_code_to_glyph          = $obj_ttf->getCodeToGlyph();
					unset( $str_code_to_glyph[0] );

					// Type0 Font
					// A composite font - a font composed of other fonts, organized hierarchically
					$this->NewObject();
					$this->Out( '<</Type /Font' );
					$this->Out( '/Subtype /Type0' );
					$this->Out( '/BaseFont /' . $str_font_name . '' );
					$this->Out( '/Encoding /Identity-H' );
					$this->Out( '/DescendantFonts [' . ( $this->int_current_object + 1 ) . ' 0 R]' );
					$this->Out( '/ToUnicode ' . ( $this->int_current_object + 2 ) . ' 0 R' );
					$this->Out( '>>' );
					$this->Out( 'endobj' );

					// CIDFontType2
					// A CIDFont whose glyph descriptions are based on TrueType font technology
					$this->NewObject();
					$this->Out( '<</Type /Font' );
					$this->Out( '/Subtype /CIDFontType2' );
					$this->Out( '/BaseFont /' . $str_font_name . '' );
					$this->Out( '/CIDSystemInfo ' . ( $this->int_current_object + 2 ) . ' 0 R' );
					$this->Out( '/FontDescriptor ' . ( $this->int_current_object + 3 ) . ' 0 R' );
					if ( isset( $arr_font_data['desc']['MissingWidth'] ) ) {
						$this->Out( '/DW ' . $arr_font_data['desc']['MissingWidth'] . '' );
					}

					$this->PutTTFontWidths( $arr_font_data, $obj_ttf->getMaxUni() );

					$this->Out( '/CIDToGIDMap ' . ( $this->int_current_object + 4 ) . ' 0 R' );
					$this->Out( '>>' );
					$this->Out( 'endobj' );

					// ToUnicode
					$this->NewObject();
					$str_to_unicode = "/CIDInit /ProcSet findresource begin\n";
					$str_to_unicode .= "12 dict begin\n";
					$str_to_unicode .= "begincmap\n";
					$str_to_unicode .= "/CIDSystemInfo\n";
					$str_to_unicode .= "<</Registry (Adobe)\n";
					$str_to_unicode .= "/Ordering (UCS)\n";
					$str_to_unicode .= "/Supplement 0\n";
					$str_to_unicode .= ">> def\n";
					$str_to_unicode .= "/CMapName /Adobe-Identity-UCS def\n";
					$str_to_unicode .= "/CMapType 2 def\n";
					$str_to_unicode .= "1 begincodespacerange\n";
					$str_to_unicode .= "<0000> <FFFF>\n";
					$str_to_unicode .= "endcodespacerange\n";
					$str_to_unicode .= "1 beginbfrange\n";
					$str_to_unicode .= "<0000> <FFFF> <0000>\n";
					$str_to_unicode .= "endbfrange\n";
					$str_to_unicode .= "endcmap\n";
					$str_to_unicode .= "CMapName currentdict /CMap defineresource pop\n";
					$str_to_unicode .= "end\n";
					$str_to_unicode .= "end";
					$this->Out( '<</Length ' . ( strlen( $str_to_unicode ) ) . '>>' );
					$this->PutStream( $str_to_unicode );
					$this->Out( 'endobj' );

					// CIDSystemInfo dictionary
					$this->NewObject();
					$this->Out( '<</Registry (Adobe)' );
					$this->Out( '/Ordering (UCS)' );
					$this->Out( '/Supplement 0' );
					$this->Out( '>>' );
					$this->Out( 'endobj' );

					// Font descriptor
					$this->NewObject();
					$this->Out( '<</Type /FontDescriptor' );
					$this->Out( '/FontName /' . $str_font_name );
					foreach ( $arr_font_data['desc'] as $str_inner_inner_key => $str_value ) {
						if ( $str_inner_inner_key == 'Flags' ) {
							$str_value = $str_value | 4;
							$str_value = $str_value & ~32;
						}   // SYMBOLIC font flag
						$this->Out( ' /' . $str_inner_inner_key . ' ' . $str_value );
					}
					$this->Out( '/FontFile2 ' . ( $this->int_current_object + 2 ) . ' 0 R' );
					$this->Out( '>>' );
					$this->Out( 'endobj' );

					// Embed CIDToGIDMap
					// A specification of the mapping from CIDs to glyph indices
					$str_cid_to_gid_map = str_pad( '', 256 * 256 * 2, "\x00" );
					foreach ( $str_code_to_glyph as $cc => $str_clyph ) {
						$str_cid_to_gid_map[ $cc * 2 ]     = chr( $str_clyph >> 8 );
						$str_cid_to_gid_map[ $cc * 2 + 1 ] = chr( $str_clyph & 0xFF );
					}
					$str_cid_to_gid_map = gzcompress( $str_cid_to_gid_map );
					$this->NewObject();
					$this->Out( '<</Length ' . strlen( $str_cid_to_gid_map ) . '' );
					$this->Out( '/Filter /FlateDecode' );
					$this->Out( '>>' );
					$this->PutStream( $str_cid_to_gid_map );
					$this->Out( 'endobj' );

					//Font file
					$this->NewObject();
					$this->Out( '<</Length ' . strlen( $str_font_stream_compressed ) );
					$this->Out( '/Filter /FlateDecode' );
					$this->Out( '/Length1 ' . $int_ttf_font_size );
					$this->Out( '>>' );
					$this->PutStream( $str_font_stream_compressed );
					$this->Out( 'endobj' );
					unset( $obj_ttf );
				} else {
					// Allow for additional types
					$this->arr_fonts[ $str_key ]['n'] = $this->int_current_object + 1;
					$str_method                       = '_put' . strtolower( $str_type );
					if ( ! method_exists( $this, $str_method ) ) {
						throw new WOE_FPDF_Exception( 'Unsupported font type: ' . esc_html($str_type), esc_html(WOE_FPDF_Exception::UNSUPPORTED_FONT) );
					}
					$this->$str_method( $arr_font_data );
				}
			}
		}
	}

	protected function PutTTFontWidths( $font, $maxUni ) {
		$cachePath = $this->getCachePath();
		$cacheFile = $cachePath . self::FILE_CHARACTER_WIDTH . $font['filename'] . '.json';

		$rangeid   = 0;
		$range     = array();
		$prevcid   = - 2;
		$prevwidth = - 1;
		$interval  = false;
		$startcid  = 1;

		if ( $cachePath !== null && file_exists( $cacheFile ) ) {
			$fopen = fopen( $cacheFile, 'r' );
			$fread = fread( $fopen, filesize( $cacheFile ) );
			fclose( $fopen );

			$json = json_decode( $fread, true );

			if ( json_last_error() !== JSON_ERROR_NONE ) {
				extract( $json );
				$startcid = 128;
			}
		}

		$cwlen = $maxUni + 1;
		// for each character
		for ( $cid = $startcid; $cid < $cwlen; $cid ++ ) {
			if( !isset($font['cw'][ $cid * 2 ]) ) {
				continue;
			}
				
			if ( $font['cw'][ $cid * 2 ] == "\00" && $font['cw'][ $cid * 2 + 1 ] == "\00" ) {
				continue;
			}

			$width = ( ord( $font['cw'][ $cid * 2 ] ) << 8 ) + ord( $font['cw'][ $cid * 2 + 1 ] );
			if ( $width == 65535 ) {
				$width = 0;
			}

			if ( $cid > 255 && ( ! isset( $font['subset'][ $cid ] ) || ! $font['subset'][ $cid ] ) ) {
				continue;
			}

			if ( ! isset( $font['dw'] ) || ( isset( $font['dw'] ) && $width != $font['dw'] ) ) {
				if ( $cid == ( $prevcid + 1 ) ) {
					if ( $width == $prevwidth ) {
						if ( $width == $range[ $rangeid ][0] ) {
							$range[ $rangeid ][] = $width;
						} else {
							array_pop( $range[ $rangeid ] );
							// new range
							$rangeid             = $prevcid;
							$range[ $rangeid ]   = array();
							$range[ $rangeid ][] = $prevwidth;
							$range[ $rangeid ][] = $width;
						}
						$interval                      = true;
						$range[ $rangeid ]['interval'] = true;
					} else {
						if ( $interval ) {
							// new range
							$rangeid             = $cid;
							$range[ $rangeid ]   = array();
							$range[ $rangeid ][] = $width;
						} else {
							$range[ $rangeid ][] = $width;
						}
						$interval = false;
					}
				} else {
					$rangeid             = $cid;
					$range[ $rangeid ]   = array();
					$range[ $rangeid ][] = $width;
					$interval            = false;
				}
				$prevcid   = $cid;
				$prevwidth = $width;
			}
		}

		// write file
		if ( $cachePath !== null && ! file_exists( $cacheFile ) ) {
			$fh = fopen( $cacheFile, 'wb' );

			$cw127 = [
				'rangeid'   => $rangeid,
				'prevcid'   => $prevcid,
				'prevwidth' => $prevwidth,
				'interval'  => (bool) $interval,
				'range'     => $range,
			];

			fwrite( $fh, json_encode( $cw127 ) );
			fclose( $fh );
		}

		$prevk   = - 1;
		$nextk   = - 1;
		$prevint = false;
		foreach ( $range as $k => $ws ) {
			$cws = count( $ws );
			if ( ( $k == $nextk ) AND ( ! $prevint ) AND ( ( ! isset( $ws['interval'] ) ) OR ( $cws < 4 ) ) ) {
				if ( isset( $range[ $k ]['interval'] ) ) {
					unset( $range[ $k ]['interval'] );
				}
				$range[ $prevk ] = array_merge( $range[ $prevk ], $range[ $k ] );
				unset( $range[ $k ] );
			} else {
				$prevk = $k;
			}
			$nextk = $k + $cws;
			if ( isset( $ws['interval'] ) ) {
				if ( $cws > 3 ) {
					$prevint = true;
				} else {
					$prevint = false;
				}
				unset( $range[ $k ]['interval'] );
				-- $nextk;
			} else {
				$prevint = false;
			}
		}
		$w = '';
		foreach ( $range as $k => $ws ) {
			if ( count( array_count_values( $ws ) ) == 1 ) {
				$w .= ' ' . $k . ' ' . ( $k + count( $ws ) - 1 ) . ' ' . $ws[0];
			} else {
				$w .= ' ' . $k . ' [ ' . implode( ' ', $ws ) . ' ]' . "\n";
			}
		}

		$this->Out( '/W [' . $w . ' ]' );
	}

	/**
	 *
	 */
	private function PutImages() {
		foreach ( array_keys( $this->arr_images ) as $str_file ) {
			$this->PutImage( $this->arr_images[ $str_file ] );
			unset( $this->arr_images[ $str_file ]['data'] );
			unset( $this->arr_images[ $str_file ]['smask'] );
		}
	}

	/**
	 * @param $arr_info
	 */
	public function PutImage( &$arr_info ) {
		$this->NewObject();
		$arr_info['n'] = $this->int_current_object;
		$this->Out( '<</Type /XObject' );
		$this->Out( '/Subtype /Image' );
		$this->Out( '/Width ' . $arr_info['w'] );
		$this->Out( '/Height ' . $arr_info['h'] );
		if ( $arr_info['cs'] == 'Indexed' ) {
			$this->Out( '/ColorSpace [/Indexed /DeviceRGB ' . ( strlen( $arr_info['pal'] ) / 3 - 1 ) . ' ' . ( $this->int_current_object + 1 ) . ' 0 R]' );
		} else {
			$this->Out( '/ColorSpace /' . $arr_info['cs'] );
			if ( $arr_info['cs'] == 'DeviceCMYK' ) {
				$this->Out( '/Decode [1 0 1 0 1 0 1 0]' );
			}
		}
		$this->Out( '/BitsPerComponent ' . $arr_info['bpc'] );
		if ( isset( $arr_info['f'] ) ) {
			$this->Out( '/Filter /' . $arr_info['f'] );
		}
		if ( isset( $arr_info['dp'] ) ) {
			$this->Out( '/DecodeParms <<' . $arr_info['dp'] . '>>' );
		}
		if ( isset( $arr_info['trns'] ) && is_array( $arr_info['trns'] ) ) {
			$trns = '';
			for ( $i = 0; $i < count( $arr_info['trns'] ); $i ++ ) {
				$trns .= $arr_info['trns'][ $i ] . ' ' . $arr_info['trns'][ $i ] . ' ';
			}
			$this->Out( '/Mask [' . $trns . ']' );
		}
		if ( isset( $arr_info['smask'] ) ) {
			$this->Out( '/SMask ' . ( $this->int_current_object + 1 ) . ' 0 R' );
		}
		$this->Out( '/Length ' . strlen( $arr_info['data'] ) . '>>' );
		$this->PutStream( $arr_info['data'] );
		$this->Out( 'endobj' );
		// Soft mask
		if ( isset( $arr_info['smask'] ) ) {
			$str_dp    = '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns ' . $arr_info['w'];
			$arr_smask = array(
				'w'    => $arr_info['w'],
				'h'    => $arr_info['h'],
				'cs'   => 'DeviceGray',
				'bpc'  => 8,
				'f'    => $arr_info['f'],
				'dp'   => $str_dp,
				'data' => $arr_info['smask']
			);
			$this->PutImage( $arr_smask );
		}
		// Palette
		if ( $arr_info['cs'] == 'Indexed' ) {
			$str_filter = ( $this->bol_compress ) ? '/Filter /FlateDecode ' : '';
			$str_pal    = ( $this->bol_compress ) ? gzcompress( $arr_info['pal'] ) : $arr_info['pal'];
			$this->NewObject();
			$this->Out( '<<' . $str_filter . '/Length ' . strlen( $str_pal ) . '>>' );
			$this->PutStream( $str_pal );
			$this->Out( 'endobj' );
		}
	}

	/**
	 *
	 */
	public function PutXObjectDict() {
		foreach ( $this->arr_images as $arr_image ) {
			$this->Out( '/I' . $arr_image['i'] . ' ' . $arr_image['n'] . ' 0 R' );
		}
	}

	/**
	 *
	 */
	public function PutResourceDict() {
		$this->Out( '/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]' );
		$this->Out( '/Font <<' );
		foreach ( $this->arr_fonts as $arr_font ) {
			$this->Out( '/F' . $arr_font['i'] . ' ' . $arr_font['n'] . ' 0 R' );
		}
		$this->Out( '>>' );
		$this->Out( '/XObject <<' );
		$this->PutXObjectDict();
		$this->Out( '>>' );
	}

	/**
	 *
	 */
	public function PutResources() {
		$this->PutFonts();
		$this->PutImages();
		// Resource dictionary
		$this->arr_offsets[2] = strlen( $this->str_buffer );
		$this->Out( '2 0 obj' );
		$this->Out( '<<' );
		$this->PutResourceDict();
		$this->Out( '>>' );
		$this->Out( 'endobj' );
	}

	/**
	 *
	 */
	public function PutInfo() {
		$this->Out( '/Producer ' . $this->TextString( 'tFPDF ' . $this->str_pdf_version ) );
		if ( ! empty( $this->str_title ) ) {
			$this->Out( '/Title ' . $this->TextString( $this->str_title ) );
		}
		if ( ! empty( $this->str_subject ) ) {
			$this->Out( '/Subject ' . $this->TextString( $this->str_subject ) );
		}
		if ( ! empty( $this->str_author ) ) {
			$this->Out( '/Author ' . $this->TextString( $this->str_author ) );
		}
		if ( ! empty( $this->str_keywords ) ) {
			$this->Out( '/Keywords ' . $this->TextString( $this->str_keywords ) );
		}
		if ( ! empty( $this->str_creator ) ) {
			$this->Out( '/Creator ' . $this->TextString( $this->str_creator ) );
		}
		$this->Out( '/CreationDate ' . $this->TextString( 'D:' . @gmdate( 'YmdHis' ) ) );
	}

	/**
	 *
	 */
	public function PutCatalog() {
		$this->Out( '/Type /Catalog' );
		$this->Out( '/Pages 1 0 R' );
		if ( $this->mix_zoom_mode == 'fullpage' ) {
			$this->Out( '/OpenAction [3 0 R /Fit]' );
		} elseif ( $this->mix_zoom_mode == 'fullwidth' ) {
			$this->Out( '/OpenAction [3 0 R /FitH null]' );
		} elseif ( $this->mix_zoom_mode == 'real' ) {
			$this->Out( '/OpenAction [3 0 R /XYZ null null 1]' );
		} elseif ( ! is_string( $this->mix_zoom_mode ) ) {
			$this->Out( '/OpenAction [3 0 R /XYZ null null ' . sprintf( '%.2F', $this->mix_zoom_mode / 100 ) . ']' );
		}
		if ( $this->str_layout_mode == 'single' ) {
			$this->Out( '/PageLayout /SinglePage' );
		} elseif ( $this->str_layout_mode == 'continuous' ) {
			$this->Out( '/PageLayout /OneColumn' );
		} elseif ( $this->str_layout_mode == 'two' ) {
			$this->Out( '/PageLayout /TwoColumnLeft' );
		}
	}

	/**
	 *
	 */
	public function PutHeader() {
		$this->Out( '%PDF-' . $this->str_pdf_version );
	}

	/**
	 *
	 */
	public function PutTrailer() {
		$this->Out( '/Size ' . ( $this->int_current_object + 1 ) );
		$this->Out( '/Root ' . $this->int_current_object . ' 0 R' );
		$this->Out( '/Info ' . ( $this->int_current_object - 1 ) . ' 0 R' );
	}

	/**
	 *
	 */
	public function EndDoc() {
		$this->PutHeader();
		$this->PutPages();
		$this->PutResources();
		// Info
		$this->NewObject();
		$this->Out( '<<' );
		$this->PutInfo();
		$this->Out( '>>' );
		$this->Out( 'endobj' );
		// Catalog
		$this->NewObject();
		$this->Out( '<<' );
		$this->PutCatalog();
		$this->Out( '>>' );
		$this->Out( 'endobj' );
		// Cross-ref
		$int_buffer_size = strlen( $this->str_buffer );
		$this->Out( 'xref' );
		$this->Out( '0 ' . ( $this->int_current_object + 1 ) );
		$this->Out( '0000000000 65535 f ' );
		for ( $i = 1; $i <= $this->int_current_object; $i ++ ) {
			$this->Out( sprintf( '%010d 00000 n ', $this->arr_offsets[ $i ] ) );
		}
		// Trailer
		$this->Out( 'trailer' );
		$this->Out( '<<' );
		$this->PutTrailer();
		$this->Out( '>>' );
		$this->Out( 'startxref' );
		$this->Out( $int_buffer_size );
		$this->Out( '%%EOF' );
		$this->int_state = self::DOCUMENT_STATE_TERMINATED;
	}

	/**
	 * Converts UTF-8 strings to UTF16-BE
	 *
	 * @param      $str_input
	 * @param bool $bol_set_byte_order_mark
	 *
	 * @return string
	 */
	public function UTF8ToUTF16BE( $str_input, $bol_set_byte_order_mark = true ) {
		$str_output = "";
		if ( $bol_set_byte_order_mark ) {
			$str_output .= "\xFE\xFF";
		}
		$str_output .= mb_convert_encoding( $str_input, 'UTF-16BE', 'UTF-8' );

		return $str_output;
	}

	/**
	 * Converts UTF-8 strings to codepoints array
	 *
	 * @param $str_input
	 *
	 * @return array
	 */
	public function UTF8StringToArray( $str_input ) {
		$arr_output        = array();
		$int_string_length = strlen( $str_input );
		for ( $i = 0; $i < $int_string_length; $i ++ ) {
			$uni      = - 1;
			$int_char = ord( $str_input[ $i ] );
			if ( $int_char <= 0x7F ) {
				$uni = $int_char;
			} elseif ( $int_char >= 0xC2 ) {
				if ( ( $int_char <= 0xDF ) && ( $i < $int_string_length - 1 ) ) {
					$uni = ( $int_char & 0x1F ) << 6 | ( ord( $str_input[ ++ $i ] ) & 0x3F );
				} elseif ( ( $int_char <= 0xEF ) && ( $i < $int_string_length - 2 ) ) {
					$uni = ( $int_char & 0x0F ) << 12 | ( ord( $str_input[ ++ $i ] ) & 0x3F ) << 6 | ( ord( $str_input[ ++ $i ] ) & 0x3F );
				} elseif ( ( $int_char <= 0xF4 ) && ( $i < $int_string_length - 3 ) ) {
					$uni = ( $int_char & 0x0F ) << 18 | ( ord( $str_input[ ++ $i ] ) & 0x3F ) << 12 | ( ord( $str_input[ ++ $i ] ) & 0x3F ) << 6 | ( ord( $str_input[ ++ $i ] ) & 0x3F );
				}
			}
			if ( $uni >= 0 ) {
				$arr_output[] = $uni;
			}
		}

		return $arr_output;
	}

	/**
	 * With this method you can set the cache path
	 *
	 * @param string $cachePath The cache folder
	 *
	 * @return string The newly set cache folder
	 */
	public function setCachePath( $cachePath = null ) {
		if ( ! file_exists( $cachePath ) ) {
			// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
			@mkdir( $cachePath, 0775, true );
		}

		// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
		if ( ! file_exists( $cachePath ) || ! is_dir( $cachePath ) || ! is_writable( $cachePath ) ) {
			throw new WOE_FPDF_Exception( 'Could not write to cache folder: ' . esc_html($cachePath), esc_html(WOE_FPDF_Exception::INVALID_CACHE_FOLDER) );
		}

		$this->cachePath = realpath( $cachePath ) . '/';

		return $cachePath;
	}

	/**
	 * Get the currently set cache folder
	 *
	 * @return string The cache folder, may be null
	 */
	public function getCachePath() {
		return $this->cachePath;
	}

	/**
	 * Clear all cached files in the cache folder
	 *
	 */
	public function clearCache() {
		$cachePath = $this->getCachePath();
		if ( $cachePath !== null ) {
			$cacheFiles = glob( $cachePath . '*.*' );
			foreach ( $cacheFiles as $cacheFile ) {
				@wp_delete_file( $cacheFile );
			}
		}
	}

	/**
	 * Calculates the expected line height for a multi cell,
	 *      useful if you want to know how much space a cell is going to use
	 *
	 * @param string $text The text to calculate the line height from
	 * @param int    $cellWidth The width of the multicell
	 * @param int    $lineHeight The line height of the multicell
	 *
	 * @return int The calculated line height
	 */
	public function calculateHeight( $text, $cellWidth = 80, $lineHeight = 3 ) {
		$explode = explode( "\r", $text );

		array_walk( $explode, 'trim' );

		$lines = [];
		foreach ( $explode as $split ) {
			$sub  = $split;
			$char = 1;

			while ( $char <= strlen( $sub ) ) {
				$substr = substr( $sub, 0, $char );

				if ( $this->getStringWidth( $substr ) >= $cellWidth - 1 ) {
					$pos = strrpos( $substr, " " );

					$lines[] = substr( $sub, 0, ( $pos !== false ? $pos : $char ) ) . ( $pos === false ? '-' : '' );

					if ( $pos !== false ) { //if $pos returns FALSE, substr has no whitespace, so split word on current position
						$char = $pos + 1;
						$len  = $char;
					}

					$sub  = ltrim( substr( $sub, $char ) );
					$char = 0;
				}

				$char ++;
			}

			if ( ! empty( $sub ) ) {
				$lines[] = $sub;
			}
		}

		return (int) count( $lines ) * $lineHeight;
	}
}
