Как заставить jsPDF.js корректно создавать PDF с кириллическими символами?

Добрый день, столкнулся с проблемой при работе с jsPDF.js, данная библиотека не работает с UTF-8. Может кто сталкивался и знает как заставить ее корректно создавать PDF с кириллическими символами?
  • Вопрос задан
  • 26493 просмотра
Пригласить эксперта
Ответы на вопрос 10
max7
@max7
max7
Если я правильно понял, что за jsPDF имеется в виду. То по простому никак. Это связано с вариантами поддержки юникода в pdf и разными версиями pdf. Ведь pdf (до версии 1.4 точно) должен поддерживаться принтерами без установленных на них шрифтов, т.е. они должны быть внедрены в документ. Нюансов очень много. Но.

Попробуйте добавить в код jsPDF следующие функции:

Вариант функции pdfEscape
var padz = 
[
   "",
   "0",
   "00",
   "000",
   "0000"
];
var pdfEscape16 = function(text) 
{
   var ar = ["FEFF"];
   for(var i = 0, l = text.length, t; i < l; ++i)
   {
      t = text.charCodeAt(i).toString(16).toUpperCase();
      ar.push(padz[4 - t.length], t);
   }
   return ar.join("");
};


В объект в конце добавить функцию
text16: function(x, y, text) 
{
   // need page height
   if(pageFontSize != fontSize) 
   {
      out('BT /F1 ' + parseInt(fontSize) + '.00 Tf ET');
      pageFontSize = fontSize;
   }
   var str = sprintf('BT %.2f %.2f Td <%s> Tj ET', x * k, (pageHeight - y) * k, pdfEscape16(text));
   out(str);
}


и добавлять текст через вызов функции text16.
Ответ написан
max7
@max7
max7
Вот, есть аналогичный вопрос на RSDN:
Вывод Unicode текста в PDF.
Ответ написан
Комментировать
max7
@max7
max7
Точно, смотрел старую версию...
Смотрим git, прямо по коду

Строка 1149: API.text = function (text, x, y, flags) {
Параметр flags

Строка 1202: str = pdfEscape(text, flags);
Функция pdfEscape

Строка 801: pdfEscape = function (text, flags) {
Параметр flags

Строка 811: return to8bitStream(text, flags)
Функция to8bitStream

Строка 659: to8bitStream = function (text, flags) {
Параметр flags

Строка 660-707: Комментарий про юникод.

/* PDF 1.3 spec:
"For text strings encoded in Unicode, the first two bytes must be 254 followed by
255, representing the Unicode byte order marker, U+FEFF. (This sequence conflicts
with the PDFDocEncoding character sequence thorn ydieresis, which is unlikely
to be a meaningful beginning of a word or phrase.) The remainder of the
string consists of Unicode character codes, according to the UTF-16 encoding
specified in the Unicode standard, version 2.0. Commonly used Unicode values
are represented as 2 bytes per character, with the high-order byte appearing first
in the string."

In other words, if there are chars in a string with char code above 255, we
recode the string to UCS2 BE - string doubles in length and BOM is prepended.

HOWEVER!
Actual *content* (body) text (as opposed to strings used in document properties etc)
does NOT expect BOM. There, it is treated as a literal GID (Glyph ID)

Because of Adobe's focus on "you subset your fonts!" you are not supposed to have
a font that maps directly Unicode (UCS2 / UTF16BE) code to font GID, but you could
fudge it with "Identity-H" encoding and custom CIDtoGID map that mimics Unicode
code page. There, however, all characters in the stream are treated as GIDs,
including BOM, which is the reason we need to skip BOM in content text (i.e. that
that is tied to a font).

To signal this "special" PDFEscape / to8bitStream handling mode,
API.text() function sets (unless you overwrite it with manual values
given to API.text(.., flags) )
flags.autoencode = true
flags.noBOM = true

*/

 /*
`flags` properties relied upon:
.sourceEncoding = string with encoding label.
"Unicode" by default. = encoding of the incoming text.
pass some non-existing encoding name
(ex: 'Do not touch my strings! I know what I am doing.')
to make encoding code skip the encoding step.
.outputEncoding = Either valid PDF encoding name
(must be supported by jsPDF font metrics, otherwise no encoding)
or a JS object, where key = sourceCharCode, value = outputCharCode
missing keys will be treated as: sourceCharCode === outputCharCode
.noBOM
See comment higher above for explanation for why this is important
.autoencode
See comment higher above for explanation for why this is important
*/


Пока всё что разобрал, посмотрю ещё...
Ответ написан
Комментировать
@KREGI Автор вопроса
указан шрифт helvetica в коде, а он Unicode поддерживает? в списке на википедии (http://ru.wikipedia.org/wiki/%D0%A8%D1%80%D0%B8%D1%84%D1%82%D1%8B,_%D0%BF%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%B8%D0%B2%D0%B0%D1%8E%D1%89%D0%B8%D0%B5_%D1%8E%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4) его нет
Ответ написан
Комментировать
max7
@max7
max7
Вот здест Unicode in PDF, пишут
See Appendix D (page 995) of the PDF specification. There is a limited number of fonts and
character sets pre-defined in a PDF consumer application. To display other characters you 
need to embed a font that contains them. It is also preferable to embed only a subset of 
the font, including only required characters, in order to reduce file size. 
I am also working on displaying Unicode characters in PDF and it is a major hassle.
adobe.com/devnet/pdf/pdf_reference.html
.
Нету в стандартных шрифтах кириллицы.
В jsPDF нет средств внедрения шрифтов.
Например, mPDF (PHP), так и поступает, внедряет подмножество шрифта.
Т. е. "просмоторщик" pdf должен сам содержать/предоставлять юникодные шрифты.
Предположим наш "просмоторщик" такие шрифты даёт.

Добавьте в файл jspdf.js перед строкой 1912 (return API; функция jsPDF) код
var padz = 
[
   "",
   "0",
   "00",
   "000",
   "0000"
];
var pdfEscape16 = function(text, flags) 
{
   var ar = ["FEFF"];
   for(var i = 0, l = text.length, t; i < l; ++i)
   {
      t = text.charCodeAt(i).toString(16).toUpperCase();
      ar.push(padz[4 - t.length], t);
   }
   return ar.join("");
};

API.text16 = function (text, x, y, flags) 
{
   /**
   * Inserts something like this into PDF
   BT
   /F1 16 Tf % Font name + size
   16 TL % How many units down for next line in multiline text
   0 g % color
   28.35 813.54 Td % position
   (line one) Tj
   T* (line two) Tj
   T* (line three) Tj
   ET
   */

   var undef, _first, _second, _third, newtext, str, i;
   // Pre-August-2012 the order of arguments was function(x, y, text, flags)
   // in effort to make all calls have similar signature like
   // function(data, coordinates... , miscellaneous)
   // this method had its args flipped.
   // code below allows backward compatibility with old arg order.
   if (typeof text === 'number') {
       _first = y;
       _second = text;
       _third = x;

       text = _first;
       x = _second;
       y = _third;
   }

   // If there are any newlines in text, we assume
   // the user wanted to print multiple lines, so break the
   // text up into an array. If the text is already an array,
   // we assume the user knows what they are doing.
   if (typeof text === 'string' && text.match(/[\n\r]/)) {
       text = text.split(/\r\n|\r|\n/g);
   }

   if (typeof flags === 'undefined') {
       flags = {'noBOM': true, 'autoencode': true};
   } else {

       if (flags.noBOM === undef) {
           flags.noBOM = true;
       }

       if (flags.autoencode === undef) {
           flags.autoencode = true;
       }

   }

   if (typeof text === 'string') {
       str = pdfEscape16(text, flags);
   } else if (text instanceof Array) { /* Array */
       // we don't want to destroy original text array, so cloning it
       newtext = text.concat();
       // we do array.join('text that must not be PDFescaped")
       // thus, pdfEscape each component separately
       for (i = newtext.length - 1; i !== -1; i--) {
           newtext[i] = pdfEscape16(newtext[i], flags);
       }
       str = newtext.join("> Tj\nT* <");
   } else {
       throw new Error('Type of text must be string or Array. "' + text + '" is not recognized.');
   }
   // Using "'" ("go next line and render text" mark) would save space but would complicate our rendering code, templates

   // BT .. ET does NOT have default settings for Tf. You must state that explicitely every time for BT .. ET
   // if you want text transformation matrix (+ multiline) to work reliably (which reads sizes of things from font declarations)
   // Thus, there is NO useful, *reliable* concept of "default" font for a page.
   // The fact that "default" (reuse font used before) font worked before in basic cases is an accident
   // - readers dealing smartly with brokenness of jsPDF's markup.
   out(
       'BT\n/' +
           activeFontKey + ' ' + activeFontSize + ' Tf\n' + // font face, style, size
           (activeFontSize * lineHeightProportion) + ' TL\n' + // line spacing
           textColor +
           '\n' + f2(x * k) + ' ' + f2((pageHeight - y) * k) + ' Td\n<' +
           str +
           '> Tj\nET'
   );
   
   return this;
};

Добавлять текст через вызов функции text16.
Ответ написан
max7
@max7
max7
Вот, например, в pdfjs (для node.js), добавление шрифта в документ:

pdfjs

Функция TTFFont.Subset.prototype.embed строка 1065.
Со строки 1099 - 1143. // unicode map
И т.д.
Ответ написан
max7
@max7
max7
Попробуйте во этот проект: Create PDFs in your browser (port of libharu)
Ответ написан
Комментировать
@6yp9T
Мы в своё время столкнулись с аналогичной проблемой, решили следующим образом:
1 - рисуем текст на canvas;
2 - canvas в картинку;
3 - картинку в jsPDF
Ответ написан
6yp9T, У Вас найдется наглядный пример?
Ответ написан
Комментировать
@Dmaw
Сделать скрин текста с помощью html2canvas и передать картинку в PDF.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы