Pull to refresh

Свой Web-PDF принтер за 10 минут

Reading time 9 min
Views 4.5K


Как потратить совсем немного времени и сделать что-нибудь простое и оригинальное, поражающее своей глобальностью — но абсолютно бесполезное? Очень просто. Давайте сделаем свой принтер.

Нам понадобится (кроме головы и рук) только работающий web-сервер с поддержкой cgi-bin, к которому у нас есть доступ по FTP. Есть такой? Поехали!


Создаем в папочке cgi-bin скрипт printer. Содержание скрипта очень простое:

<br>#! /usr/bin/perl<br><br>use strict;<br><br>if (!defined($ENV{'CONTENT_TYPE'}) || $ENV{'CONTENT_TYPE'} ne "application/ipp") {<br>    print "Content-Type: text/html\n\n";<br>    print ":-)";<br>    exit;<br>}<br><br># $d - входные данные в виде строки<br># $l - длина строки входных данных<br># $i - текущая позиция разбора данных<br># %a - разобранные атрибуты<br><br>my ($d, $l, $i, %a) = ("", 0, 0);<br>binmode STDIN;<br>$l += read(STDIN, $d, 4096, $l) while (!eof(STDIN));<br><br>parseRequest(\$d, \$l, \$i, \%a);<br><br>my $o = ""<br> . substr($d, 0, 2) # version<br> . chr(0x00) . chr(0x00) # status<br> . substr($d, 4, 4) # request<br> . chr(0x01) # attributes<br> . stringAttribute(0x47, "attributes-charset", "utf-8")<br> . stringAttribute(0x48, "attributes-natural-language", "en-us")<br> . chr(0x04) # attributes<br> . stringAttribute(0x42, "printer-name", "PDF")<br> . chr(0x03) # end<br> . chr(0x0a)<br>;<br><br>print "Content-Type: text/html\n";<br>print "Content-Length: " . length($o) . "\n";<br>print "\n";<br>print $o;<br><br>if (defined($a{'-status'}) && $a{'-status'} == 0x02 && $i < $l) {<br>    my @t = localtime;<br>    my $output = sprintf("../pdf/%04d%02d%02d-%02d%02d%02d.pdf", $t[5] + 1900, $t[4], $t[3], $t[2], $t[1], $t[0]);<br>    if (open(P, "|-", "gs", "-q", "-dBATCH", "-dNOPAUSE", "-dSAFER", "-sDEVICE=pdfwrite", "-sOutputFile=$output", "-")) {<br>        binmode P;<br>        print P substr($d, $i);<br>        close P;<br>    }<br>}<br><br>sub parseRequest {<br>    my ($d, $l, $i, $a) = @_;<br>    return if $$i >= $$l - 2;<br>    $$a{'-version'} = (ord(substr($$d, $$i, 1)) << 8) + ord(substr($$d, $$i + 1, 1)); $$i += 2;<br>    return if $$i >= $$l - 2;<br>    $$a{'-status'} = (ord(substr($$d, $$i, 1)) << 8) + ord(substr($$d, $$i + 1, 1)); $$i += 2;<br>    return if $$i >= $$l - 4;<br>    $$a{'-request'} = parseInt(substr($$d, $$i, 4)); $$i += 4;<br>    return if $$i >= $$l - 1;<br>    my $what = ord(substr($$d, $$i, 1)); $$i ++;<br>    return parseAttributes($d, $l, $i, $a) if ($what == 0x01);<br>}<br><br>sub parseAttributes {<br>    my ($d, $l, $i, $a) = @_;<br>    while ($$i < $$l) {<br>        my $what = ord(substr($$d, $$i, 1)); $$i ++;<br>        return if ($what == 0x03);<br>        return parseAttributes($d, $l, $i, $a) if ($what == 0x02);<br>        return parseAttributes($d, $l, $i, $a) if ($what == 0x04);<br>        return if $$i >= $$l - 2;<br>        my $key_len = (ord(substr($$d, $$i, 1)) << 8) + ord(substr($$d, $$i + 1, 1)); $$i += 2;<br>        return if $$i >= $$l - $key_len;<br>        my $key = substr($$d, $$i, $key_len); $$i += $key_len;<br>        return if $$i >= $$l - 2;<br>        my $val_len = (ord(substr($$d, $$i, 1)) << 8) + ord(substr($$d, $$i + 1, 1)); $$i += 2;<br>        return if $$i >= $$l - $val_len;<br>        my $val = substr($$d, $$i, $val_len); $$i += $val_len;<br>        $$a{$key} = $val;<br>    }<br>}<br><br>sub parseInt {<br>    my $v = shift;<br>    my $l = length($v);<br>    my $r = 0;<br>    for (my $i = $l; $i > 0; $i --) {<br>        $r += ( (1 << (($i - 1) * 8)) * ord(substr($v, $l - $i, 1)) );<br>    }<br>    $r -= 4294967296 if ($r >= 2147483648);<br>    return $r;<br>}<br><br>sub stringLength {<br>    my $s = shift;<br>    my $l = length($s);<br>    my $i1 = $l & 0xFF;<br>    $l = ($l - $i1) >> 8;<br>    my $i2 = $l & 0xFF;<br>    return chr($i2) . chr($i1);<br>}<br><br>sub stringAttribute {<br>    my ($type, $key, $val) = @_;<br>    return chr($type) . stringLength($key) . $key . stringLength($val) . $val;<br>}<br>

Как видите, никаких внешних зависимостей от модулей Perl, и ничего лишнего. Фактически, нужны только Perl и программа gs, которые есть практически везде.

Скрипт когда-то был основан на PHP::Print::IPP. Но, так как на большинстве серверов выполнять внешние программы из PHP-скриптов запрещено, то пришлось переписать на Perl. Скрипт реализует самую-самую базовую функциональность IPP-сервера.

Далее, даем права на исполнение скрипта (755, или rwxr-xr-x). Смотрим в браузере: http://www.site.ru/cgi-bin/printer. Работает? Хорошо.

Еще создадим папку pdf в корне сайта и установим права на запись к этой папке (777, или rwxrwxrwx).

Теперь добавляем принтер в Windows:
  • Установка принтера
  • Сетевой принтер или принтер, подключенный к другому компьютеру
  • Подключиться к принтеру в Интернете, в домашней сети или в интрасети: http://www.site.ru/cgi-bin/printer
  • Изготовитель: Generic, модель: MS Publisher Imagesetter
И печатаем пробную страницу. В папочке pdf на сервере появляется наша пробная страница.

Аналогичным образом можно напечатать много чего. Документы, картинки… Все, что угодно.

Осталось сделать три замечания.

1. Теоретически, скрипт может не работать, по самым разным причинам. Основные принципы отладки Perl-скриптов оставлю на самостоятельное изучение.
2. Желательно переименовать скрипт printer в какой-нибудь более хитрый, чтобы все подряд не печатали на Вашем принтере. Через .htaccess закрыть доступ к CGI-скрипту достаточно сложно, сделать авторизацию внутри Perl-скрипта тоже. А то можно было бы...
3. Принтер нормально работает под Windows Vista и Linux. Установка, в целом, не такая уж и сложная. Но вот нужно ли это?..

UPD: По многочисленным просьбам, выкладываю версию на PHP.
Необходимо включить в php.ini always_populate_raw_post_data = On и не отключать возможность выполнять popen.

<?<br><br>if (!isset($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != "application/ipp") {<br>    header("Content-Type: text/html");<br>    print ":-)";<br>    exit;<br>}<br><br># $d - входные данные в виде строки<br># $l - длина строки входных данных<br># $i - текущая позиция разбора данных<br># %a - разобранные атрибуты<br><br>$d = &$HTTP_RAW_POST_DATA;<br>$l = strlen($d);<br>$i = 0;<br>$a = array();<br><br>parseRequest($d, $l, $i, $a);<br><br>$o = ""<br> . substr($d, 0, 2) # version<br> . chr(0x00) . chr(0x00) # status<br> . substr($d, 4, 4) # request<br> . chr(0x01) # attributes<br> . stringAttribute(0x47, "attributes-charset", "utf-8")<br> . stringAttribute(0x48, "attributes-natural-language", "en-us")<br> . chr(0x04) # attributes<br> . stringAttribute(0x42, "printer-name", "PDF")<br> . chr(0x03) # end<br> . chr(0x0a)<br>;<br><br>header("Content-Type: text/html");<br>header("Content-Length: " . strlen($o));<br>print $o;<br><br>if (isset($a['-status']) && $a['-status'] == 0x02 && $i < $l) {<br>    $output = sprintf("pdf/%s.pdf", date("Ymd-His"));<br>    if ($P = popen("gs -q -dBATCH -dNOPAUSE -dSAFER -sDEVICE=pdfwrite -sOutputFile=$output -", "w")) {<br>        fwrite($P, substr($d, $i));<br>        fclose($P);<br>    }<br>}<br><br>function parseRequest(&$d, &$l, &$i, &$a) {<br>    if ($i >= $l - 2) return;<br>    $a['-version'] = (ord($d[$i]) << 8) + ord($d[$i + 1]); $i += 2;<br>    if ($i >= $l - 2) return;<br>    $a['-status'] = (ord($d[$i]) << 8) + ord($d[$i + 1]); $i += 2;<br>    if ($i >= $l - 4) return;<br>    $a['-request'] = parseInt(substr($d, $i, 4)); $i += 4;<br>    if ($i >= $l - 1) return;<br>    $what = ord($d[$i]); $i ++;<br>    if ($what == 0x01) return parseAttributes($d, $l, $i, $a);<br>}<br><br>function parseAttributes(&$d, &$l, &$i, &$a) {<br>    while ($i < $l) {<br>        $what = ord($d[$i]); $i ++;<br>        if ($what == 0x03) return;<br>        if ($what == 0x02) return parseAttributes($d, $l, $i, $a);<br>        if ($what == 0x04) return parseAttributes($d, $l, $i, $a);<br>        if ($i >= $l - 2) return;<br>        $key_len = (ord($d[$i]) << 8) + ord($d[$i + 1]); $i += 2;<br>        if ($i >= $l - $key_len) return;<br>        $key = substr($d, $i, $key_len); $i += $key_len;<br>        if ($i >= $l - 2) return;<br>        $val_len = (ord($d[$i]) << 8) + ord($d[$i + 1]); $i += 2;<br>        if ($i >= $l - $val_len) return;<br>        $val = substr($d, $i, $val_len); $i += $val_len;<br>        $a[$key] = $val;<br>    }<br>}<br><br>function parseInt($v) {<br>    $r = 0;<br>    $l = strlen($v);<br>    for ($i = $l; $i > 0; $i --) {<br>        $r += ( (1 << (($i - 1) * 8)) * ord($v[$l - $i]) );<br>    }<br>    if ($r >= 2147483648) $r -= 4294967296;<br>    return $r;<br>}<br><br>function stringLength($s) {<br>    $l = strlen($s);<br>    $i1 = $l & 0xFF;<br>    $l = ($l - $i1) >> 8;<br>    $i2 = $l & 0xFF;<br>    return chr($i2) . chr($i1);<br>}<br><br>function stringAttribute($type, $key, $val) {<br>    return chr($type) . stringLength($key) . $key . stringLength($val) . $val;<br>}<br><br>?><br>
Для продвинутых пользователей. Если пробовать запускать скрипт на Windows-сервере, то, возможно, понадобится указать полный путь к интерпретатору gs. При этом слэши в пути должны быть обратными: «C:\\Program files\\\gs\\gs8.71\\bin\\gs.exe». То же самое касается файла PDF. Потому что, хоть PHP сам по себе и понимает файлы с прямыми слэшами, этого нельзя сказать об интерпретаторе командной строки Windows.
Tags:
Hubs:
+99
Comments 47
Comments Comments 47

Articles