I managed to fix these issues for my site.
1. Make a module the usual way.
app/code/MyVendor/PdfFont/etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Magento\Sales\Model\Order\Pdf\Shipment" type="MyVendor\PdfFont\Model\Order\Pdf\Shipment" />
</config>
app/code/MyVendor/PdfFont/registration.php
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'MyVendor_PdfFont',
__DIR__
);
2. Use di.xml to override the Shipment, Invoice and Creditmemo classes.
app/code/MyVendor/PdfFont/etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Magento\Sales\Model\Order\Pdf\Shipment" type="MyVendor\PdfFont\Model\Order\Pdf\Shipment" />
<preference for="Magento\Sales\Model\Order\Pdf\Invoice" type="MyVendor\PdfFont\Model\Order\Pdf\Invoice" />
<preference for="Magento\Sales\Model\Order\Pdf\Creditmemo" type="MyVendor\PdfFont\Model\Order\Pdf\Creditmemo" />
</config>
3. Add Shipment, Invoice and Creditmemo overrides.
These classes are just an exact copy of the classes from Magento but now they extend our custom class HebrewPdf instead of AbstractPdf.
Note I have only included the full Shipment class and truncated the Invoice and Creditmemo classes due to hitting stackoverflow character limit.
MyVendor\PdfFont\Model\Order\Pdf\Shipment.php
<?php
/**
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace MyVendor\PdfFont\Model\Order\Pdf;
/**
* Sales Order Shipment PDF model
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Shipment extends \MyVendor\PdfFont\Model\Order\Pdf\HebrewPdf
{
/**
* @var \Magento\Store\Model\StoreManagerInterface
*/
protected $_storeManager;
/**
* @var \Magento\Framework\Locale\ResolverInterface
*/
protected $_localeResolver;
/**
* @param \Magento\Payment\Helper\Data $paymentData
* @param \Magento\Framework\Stdlib\StringUtils $string
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
* @param \Magento\Framework\Filesystem $filesystem
* @param \Magento\Sales\Model\Order\Pdf\Config $pdfConfig
* @param \Magento\Sales\Model\Order\Pdf\Total\Factory $pdfTotalFactory
* @param \Magento\Sales\Model\Order\Pdf\ItemsFactory $pdfItemsFactory
* @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
* @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation
* @param \Magento\Sales\Model\Order\Address\Renderer $addressRenderer
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param \Magento\Framework\Locale\ResolverInterface $localeResolver
* @param array $data
*
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
\Magento\Payment\Helper\Data $paymentData,
\Magento\Framework\Stdlib\StringUtils $string,
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
\Magento\Framework\Filesystem $filesystem,
\Magento\Sales\Model\Order\Pdf\Config $pdfConfig,
\Magento\Sales\Model\Order\Pdf\Total\Factory $pdfTotalFactory,
\Magento\Sales\Model\Order\Pdf\ItemsFactory $pdfItemsFactory,
\Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
\Magento\Framework\Translate\Inline\StateInterface $inlineTranslation,
\Magento\Sales\Model\Order\Address\Renderer $addressRenderer,
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Framework\Locale\ResolverInterface $localeResolver,
array $data = []
) {
$this->_storeManager = $storeManager;
$this->_localeResolver = $localeResolver;
parent::__construct(
$paymentData,
$string,
$scopeConfig,
$filesystem,
$pdfConfig,
$pdfTotalFactory,
$pdfItemsFactory,
$localeDate,
$inlineTranslation,
$addressRenderer,
$data
);
}
/**
* Draw table header for product items
*
* @param \Zend_Pdf_Page $page
* @return void
*/
protected function _drawHeader(\Zend_Pdf_Page $page)
{
/* Add table head */
$this->_setFontRegular($page, 10);
$page->setFillColor(new \Zend_Pdf_Color_RGB(0.93, 0.92, 0.92));
$page->setLineColor(new \Zend_Pdf_Color_GrayScale(0.5));
$page->setLineWidth(0.5);
$page->drawRectangle(25, $this->y, 570, $this->y - 15);
$this->y -= 10;
$page->setFillColor(new \Zend_Pdf_Color_RGB(0, 0, 0));
//columns headers
$lines[0][] = ['text' => __('Products'), 'feed' => 100];
$lines[0][] = ['text' => __('Qty'), 'feed' => 35];
$lines[0][] = ['text' => __('SKU'), 'feed' => 565, 'align' => 'right'];
$lineBlock = ['lines' => $lines, 'height' => 10];
$this->drawLineBlocks($page, [$lineBlock], ['table_header' => true]);
$page->setFillColor(new \Zend_Pdf_Color_GrayScale(0));
$this->y -= 20;
}
/**
* Return PDF document
*
* @param \Magento\Sales\Model\Order\Shipment[] $shipments
* @return \Zend_Pdf
*/
public function getPdf($shipments = [])
{
$this->_beforeGetPdf();
$this->_initRenderer('shipment');
$pdf = new \Zend_Pdf();
$this->_setPdf($pdf);
$style = new \Zend_Pdf_Style();
$this->_setFontBold($style, 10);
foreach ($shipments as $shipment) {
if ($shipment->getStoreId()) {
$this->_localeResolver->emulate($shipment->getStoreId());
$this->_storeManager->setCurrentStore($shipment->getStoreId());
}
$page = $this->newPage();
$order = $shipment->getOrder();
/* Add image */
$this->insertLogo($page, $shipment->getStore());
/* Add address */
$this->insertAddress($page, $shipment->getStore());
/* Add head */
$this->insertOrder(
$page,
$shipment,
$this->_scopeConfig->isSetFlag(
self::XML_PATH_SALES_PDF_SHIPMENT_PUT_ORDER_ID,
\Magento\Store\Model\ScopeInterface::SCOPE_STORE,
$order->getStoreId()
)
);
/* Add document text and number */
$this->insertDocumentNumber($page, __('Packing Slip # ') . $shipment->getIncrementId());
/* Add table */
$this->_drawHeader($page);
/* Add body */
foreach ($shipment->getAllItems() as $item) {
if ($item->getOrderItem()->getParentItem()) {
continue;
}
/* Draw item */
$this->_drawItem($item, $page, $order);
$page = end($pdf->pages);
}
}
$this->_afterGetPdf();
if ($shipment->getStoreId()) {
$this->_localeResolver->revert();
}
return $pdf;
}
/**
* Create new page and assign to PDF object
*
* @param array $settings
* @return \Zend_Pdf_Page
*/
public function newPage(array $settings = [])
{
/* Add new table head */
$page = $this->_getPdf()->newPage(\Zend_Pdf_Page::SIZE_A4);
$this->_getPdf()->pages[] = $page;
$this->y = 800;
if (!empty($settings['table_header'])) {
$this->_drawHeader($page);
}
return $page;
}
}
MyVendor\PdfFont\Model\Order\Pdf\Invoice.php
<?php
/**
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace MyVendor\PdfFont\Model\Order\Pdf;
use Magento\Sales\Model\ResourceModel\Order\Invoice\Collection;
/**
* Sales Order Invoice PDF model
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Invoice extends \MyVendor\PdfFont\Model\Order\Pdf\HebrewPdf
{
// Code omitted due to hitting stack overflow character limit
// Copy the rest of the code from Magento\Sales\Model\Order\Pdf\Invoice
}
MyVendor\PdfFont\Model\Order\Pdf\Creditmemo.php
<?php
/**
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace MyVendor\PdfFont\Model\Order\Pdf;
/**
* Sales Order Creditmemo PDF model
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Creditmemo extends \MyVendor\PdfFont\Model\Order\Pdf\HebrewPdf
{
// Code omitted due to hitting stack overflow character limit
// Copy the rest of the code from Magento\Sales\Model\Order\Pdf\Creditmemo
}
4. Add HebrewPdf which extends Magento\Sales\Model\Order\Pdf\AbstractPdf
and replaces _setFontRegular()
, _setFontBold()
, _setFontItalic()
and insertOrder()
methods.
Note the use of the Arimo font, I got that from Google fonts by searching for fonts that support Hebrew.
In the insertOrder()
method I put in some code which passes text to the utf8HebrewStrrev()
method which detects and reverses Hebrew text. In my example I am only reversing Hebrew in (Order date, billing address, shipping address, payment methods, shipping methods). You may need to add detects for other parts. I have labeled all edits in this method with:-
// ---------- START: PdfFont Edit
// some code ...
// ---------- END: PdfFont Edit
To make it easy to see what's been added. compare it with the insertOrder()
found in Magento\Sales\Model\Order\Pdf\AbstractPdf
MyVendor\PdfFont\Model\Order\Pdf\HebrewPdf.php
<?php
namespace MyVendor\PdfFont\Model\Order\Pdf;
use Magento\Framework\App\Filesystem\DirectoryList;
class HebrewPdf extends \Magento\Sales\Model\Order\Pdf\AbstractPdf
{
/**
* @param \Magento\Payment\Helper\Data $paymentData
* @param \Magento\Framework\Stdlib\StringUtils $string
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
* @param \Magento\Framework\Filesystem $filesystem
* @param \Magento\Sales\Model\Order\Pdf\Config $pdfConfig
* @param \Magento\Sales\Model\Order\Pdf\Total\Factory $pdfTotalFactory
* @param \Magento\Sales\Model\Order\Pdf\ItemsFactory $pdfItemsFactory
* @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
* @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation
* @param \Magento\Sales\Model\Order\Address\Renderer $addressRenderer
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param \Magento\Framework\Locale\ResolverInterface $localeResolver
* @param array $data
*
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
\Magento\Payment\Helper\Data $paymentData,
\Magento\Framework\Stdlib\StringUtils $string,
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
\Magento\Framework\Filesystem $filesystem,
\Magento\Sales\Model\Order\Pdf\Config $pdfConfig,
\Magento\Sales\Model\Order\Pdf\Total\Factory $pdfTotalFactory,
\Magento\Sales\Model\Order\Pdf\ItemsFactory $pdfItemsFactory,
\Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
\Magento\Framework\Translate\Inline\StateInterface $inlineTranslation,
\Magento\Sales\Model\Order\Address\Renderer $addressRenderer,
array $data = []
) {
parent::__construct(
$paymentData,
$string,
$scopeConfig,
$filesystem,
$pdfConfig,
$pdfTotalFactory,
$pdfItemsFactory,
$localeDate,
$inlineTranslation,
$addressRenderer,
$data
);
}
/**
* Check for Hebrew strings, if Hebrew is found the text is reversed.
* This is because there is a bug with Zend PDF that reverses Hebrew words.
* We are preemptively reversing them so when Zend Pdf reverses them it is actually correct the words back to normal.
*/
public function utf8HebrewStrrev($str) {
preg_match('/\p{Hebrew}/u', $str, $matches);
if (count($matches) > 0) {
preg_match_all('/./us', $str, $ar);
$str = join('',array_reverse($ar[0]));
}
return $str;
}
/**
* Override from: \Magento\Sales\Model\Order\Pdf\AbstractPdf
*
* Set font as regular
*
* @param \Zend_Pdf_Page $object
* @param int $size
* @return \Zend_Pdf_Resource_Font
*/
public function _setFontRegular($object, $size = 7)
{
$font = \Zend_Pdf_Font::fontWithPath(
$this->_rootDirectory->getAbsolutePath('lib/internal/Arimo/Arimo-Regular.ttf')
);
$object->setFont($font, $size);
return $font;
}
/**
* Override from: \Magento\Sales\Model\Order\Pdf\AbstractPdf
*
* Set font as bold
*
* @param \Zend_Pdf_Page $object
* @param int $size
* @return \Zend_Pdf_Resource_Font
*/
public function _setFontBold($object, $size = 7)
{
$font = \Zend_Pdf_Font::fontWithPath(
$this->_rootDirectory->getAbsolutePath('lib/internal/Arimo/Arimo-Bold.ttf')
);
$object->setFont($font, $size);
return $font;
}
/**
* Override from: \Magento\Sales\Model\Order\Pdf\AbstractPdf
*
* Set font as italic
*
* @param \Zend_Pdf_Page $object
* @param int $size
* @return \Zend_Pdf_Resource_Font
*/
public function _setFontItalic($object, $size = 7)
{
$font = \Zend_Pdf_Font::fontWithPath(
$this->_rootDirectory->getAbsolutePath('lib/internal/Arimo/Arimo-Italic.ttf')
);
$object->setFont($font, $size);
return $font;
}
/**
* Override from: \Magento\Sales\Model\Order\Pdf\AbstractPdf
*
* Insert order to pdf page
*
* @param \Zend_Pdf_Page &$page
* @param \Magento\Sales\Model\Order $obj
* @param bool $putOrderId
* @return void
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
protected function insertOrder(&$page, $obj, $putOrderId = true)
{
if ($obj instanceof \Magento\Sales\Model\Order) {
$shipment = null;
$order = $obj;
} elseif ($obj instanceof \Magento\Sales\Model\Order\Shipment) {
$shipment = $obj;
$order = $shipment->getOrder();
}
$this->y = $this->y ? $this->y : 815;
$top = $this->y;
$page->setFillColor(new \Zend_Pdf_Color_GrayScale(0.45));
$page->setLineColor(new \Zend_Pdf_Color_GrayScale(0.45));
$page->drawRectangle(25, $top, 570, $top - 55);
$page->setFillColor(new \Zend_Pdf_Color_GrayScale(1));
$this->setDocHeaderCoordinates([25, $top, 570, $top - 55]);
$this->_setFontRegular($page, 10);
if ($putOrderId) {
$page->drawText(__('Order # ') . $order->getRealOrderId(), 35, $top -= 30, 'UTF-8');
}
// ---------- START: PdfFont Edit
// Check for Hebrew text in Order date
$orderDate = $this->_localeDate->formatDate(
$this->_localeDate->scopeDate(
$order->getStore(),
$order->getCreatedAt(),
true
),
\IntlDateFormatter::MEDIUM,
false
);
$orderDateSplit = explode(' ', $orderDate);
foreach ($orderDateSplit as $key => $value) {
$orderDateSplit[$key] = $this->utf8HebrewStrrev($value);
}
$orderDate = implode(' ', $orderDateSplit);
// ---------- END: PdfFont Edit
$page->drawText(
__('Order Date: ') .
$orderDate,
35,
$top -= 15,
'UTF-8'
);
$top -= 10;
$page->setFillColor(new \Zend_Pdf_Color_Rgb(0.93, 0.92, 0.92));
$page->setLineColor(new \Zend_Pdf_Color_GrayScale(0.5));
$page->setLineWidth(0.5);
$page->drawRectangle(25, $top, 275, $top - 25);
$page->drawRectangle(275, $top, 570, $top - 25);
/* Calculate blocks info */
/* Billing Address */
$billingAddress = $this->_formatAddress($this->addressRenderer->format($order->getBillingAddress(), 'pdf'));
/* Payment */
$paymentInfo = $this->_paymentData->getInfoBlock($order->getPayment())->setIsSecureMode(true)->toPdf();
$paymentInfo = htmlspecialchars_decode($paymentInfo, ENT_QUOTES);
$payment = explode('{{pdf_row_separator}}', $paymentInfo);
foreach ($payment as $key => $value) {
if (strip_tags(trim($value)) == '') {
unset($payment[$key]);
}
}
reset($payment);
/* Shipping Address and Method */
if (!$order->getIsVirtual()) {
/* Shipping Address */
$shippingAddress = $this->_formatAddress($this->addressRenderer->format($order->getShippingAddress(), 'pdf'));
$shippingMethod = $order->getShippingDescription();
}
$page->setFillColor(new \Zend_Pdf_Color_GrayScale(0));
$this->_setFontBold($page, 12);
$page->drawText(__('Sold to:'), 35, $top - 15, 'UTF-8');
if (!$order->getIsVirtual()) {
$page->drawText(__('Ship to:'), 285, $top - 15, 'UTF-8');
} else {
$page->drawText(__('Payment Method:'), 285, $top - 15, 'UTF-8');
}
$addressesHeight = $this->_calcAddressHeight($billingAddress);
if (isset($shippingAddress)) {
$addressesHeight = max($addressesHeight, $this->_calcAddressHeight($shippingAddress));
}
$page->setFillColor(new \Zend_Pdf_Color_GrayScale(1));
$page->drawRectangle(25, $top - 25, 570, $top - 33 - $addressesHeight);
$page->setFillColor(new \Zend_Pdf_Color_GrayScale(0));
$this->_setFontRegular($page, 10);
$this->y = $top - 40;
$addressesStartY = $this->y;
foreach ($billingAddress as $value) {
if ($value !== '') {
$text = [];
foreach ($this->string->split($value, 45, true, true) as $_value) {
$text[] = $_value;
}
foreach ($text as $part) {
// ---------- START: PdfFont Edit
// Check for Hebrew text in billing address
$part = $this->utf8HebrewStrrev($part);
// ---------- END: PdfFont Edit
$page->drawText(strip_tags(ltrim($part)), 35, $this->y, 'UTF-8');
$this->y -= 15;
}
}
}
$addressesEndY = $this->y;
if (!$order->getIsVirtual()) {
$this->y = $addressesStartY;
foreach ($shippingAddress as $value) {
if ($value !== '') {
$text = [];
foreach ($this->string->split($value, 45, true, true) as $_value) {
$text[] = $_value;
}
foreach ($text as $part) {
// ---------- START: PdfFont Edit
// check for Hebrew text in shipping address
$part = $this->utf8HebrewStrrev($part);
// ---------- END: PdfFont Edit
$page->drawText(strip_tags(ltrim($part)), 285, $this->y, 'UTF-8');
$this->y -= 15;
}
}
}
$addressesEndY = min($addressesEndY, $this->y);
$this->y = $addressesEndY;
$page->setFillColor(new \Zend_Pdf_Color_Rgb(0.93, 0.92, 0.92));
$page->setLineWidth(0.5);
$page->drawRectangle(25, $this->y, 275, $this->y - 25);
$page->drawRectangle(275, $this->y, 570, $this->y - 25);
$this->y -= 15;
$this->_setFontBold($page, 12);
$page->setFillColor(new \Zend_Pdf_Color_GrayScale(0));
$page->drawText(__('Payment Method'), 35, $this->y, 'UTF-8');
$page->drawText(__('Shipping Method:'), 285, $this->y, 'UTF-8');
$this->y -= 10;
$page->setFillColor(new \Zend_Pdf_Color_GrayScale(1));
$this->_setFontRegular($page, 10);
$page->setFillColor(new \Zend_Pdf_Color_GrayScale(0));
$paymentLeft = 35;
$yPayments = $this->y - 15;
} else {
$yPayments = $addressesStartY;
$paymentLeft = 285;
}
foreach ($payment as $value) {
if (trim($value) != '') {
//Printing "Payment Method" lines
$value = preg_replace('/<br[^>]*>/i', "\n", $value);
foreach ($this->string->split($value, 45, true, true) as $_value) {
// ---------- START: PdfFont Edit
// Check for Hebrew text in payment method
$_value = $this->utf8HebrewStrrev($_value);
// ---------- END: PdfFont Edit
$page->drawText(strip_tags(trim($_value)), $paymentLeft, $yPayments, 'UTF-8');
$yPayments -= 15;
}
}
}
if ($order->getIsVirtual()) {
// replacement of Shipments-Payments rectangle block
$yPayments = min($addressesEndY, $yPayments);
$page->drawLine(25, $top - 25, 25, $yPayments);
$page->drawLine(570, $top - 25, 570, $yPayments);
$page->drawLine(25, $yPayments, 570, $yPayments);
$this->y = $yPayments - 15;
} else {
$topMargin = 15;
$methodStartY = $this->y;
$this->y -= 15;
foreach ($this->string->split($shippingMethod, 45, true, true) as $_value) {
// ---------- START: PdfFont Edit
// check for Hebrew text in Shipping methods
$_value = $this->utf8HebrewStrrev($_value);
// ---------- END: PdfFont Edit
$page->drawText(strip_tags(trim($_value)), 285, $this->y, 'UTF-8');
$this->y -= 15;
}
$yShipments = $this->y;
$totalShippingChargesText = "(" . __(
'Total Shipping Charges'
) . " " . $order->formatPriceTxt(
$order->getShippingAmount()
) . ")";
$page->drawText($totalShippingChargesText, 285, $yShipments - $topMargin, 'UTF-8');
$yShipments -= $topMargin + 10;
$tracks = [];
if ($shipment) {
$tracks = $shipment->getAllTracks();
}
if (count($tracks)) {
$page->setFillColor(new \Zend_Pdf_Color_Rgb(0.93, 0.92, 0.92));
$page->setLineWidth(0.5);
$page->drawRectangle(285, $yShipments, 510, $yShipments - 10);
$page->drawLine(400, $yShipments, 400, $yShipments - 10);
//$page->drawLine(510, $yShipments, 510, $yShipments - 10);
$this->_setFontRegular($page, 9);
$page->setFillColor(new \Zend_Pdf_Color_GrayScale(0));
//$page->drawText(__('Carrier'), 290, $yShipments - 7 , 'UTF-8');
$page->drawText(__('Title'), 290, $yShipments - 7, 'UTF-8');
$page->drawText(__('Number'), 410, $yShipments - 7, 'UTF-8');
$yShipments -= 20;
$this->_setFontRegular($page, 8);
foreach ($tracks as $track) {
$maxTitleLen = 45;
$endOfTitle = strlen($track->getTitle()) > $maxTitleLen ? '...' : '';
$truncatedTitle = substr($track->getTitle(), 0, $maxTitleLen) . $endOfTitle;
$page->drawText($truncatedTitle, 292, $yShipments, 'UTF-8');
$page->drawText($track->getNumber(), 410, $yShipments, 'UTF-8');
$yShipments -= $topMargin - 5;
}
} else {
$yShipments -= $topMargin - 5;
}
$currentY = min($yPayments, $yShipments);
// replacement of Shipments-Payments rectangle block
$page->drawLine(25, $methodStartY, 25, $currentY);
//left
$page->drawLine(25, $currentY, 570, $currentY);
//bottom
$page->drawLine(570, $currentY, 570, $methodStartY);
//right
$this->y = $currentY;
$this->y -= 15;
}
}
// needed just because this inherits from abstract class
public function getPdf($invoices = [])
{
return false;
}
}