О том как я на Java из PDF флэш получал

в 18:31, , рубрики: flash, java, PDF, svg, Программирование, метки: , ,

image
Давным давно, когда трава была зеленее, меня поймали и долго пытали пришлось мне повышать перформанс в одной чудесной связке.

Как задачу понял архитектор

Дано: есть безумный каталог изделий некая гора PDF по паре тыщ страниц каждый. Надо их выдать в веб в виде красочных анимированных презентаций.

Попытка решения: написали плееры на флэше и на javascript, которым скармливается этот преобразованный каталог, и они разными красочными эффектами по определенному алгоритму крутят нечто рекламное.

Проблема: каталоги постоянно меняются, а конвертация одного такого гроссбуха занимает больше часа(!).

Почему так и как улучшить?

Как это сделано до нас

image
Резонен вопрос — а чего так сложно-то? Так кроме десктопов мобильники окучить обязательно, а генерировать растр для 200 устройств и потом на всем этом тестить — не наш метод. И потом — а с зумом тогда что делать?

Поэтому для десктопа — флэш в древних браузерах (ура корпоративному IE), и HTML5 для всего остального. Картинки векторные (кроме превьюшек, а то надцать SVG-шек сразу планшеты пока осиливают слабо).

Опен-сорц (как всегда) спешит на помощь.

Анализирую — что же там такое делается. Обнаруживаю вот что

  • pdftoswf (ну и что что он discontinued? зато работает)
  • pdftosvg (версию 0.1)
  • ImageMagick (помним про PNG в виде уменьшенных превьюшек)
  • pdftohtml (там ведь есть еще и текст — и надо достать подзаголовки, а из XML это делать как-то удобнее)

Запускаю это все по очереди на тестовом PDF в 500 страниц. Время работы 1 час 2 минуты.

Вот тебе, бабушка, и Юрьев день!

Кто виноват?

image
Очевидно, что парсить PDF аж четыре раза подряд — не лучший выбор.

Не менее очевидно, что ImageMagick — выбор явно не тот, 3/4 времени потрачено именно утилитой convert.

Именно в этот момент ко мне в комнату заглянул заказчик с окутанным паром утюгом и сказал: срочно нужна твоя мудрость! я вступаю в игру.

Общее направление признаем условно верным — куда деваться, релиз как всегда «еще вчера», а к плееру вопросов нет. Но вот утиль считаем подобранным неудачно, и обращаем свое внимание на Java.

Новые действующие лица

image
Берем следующий набор джентьмена:

  • PDFRenderer
  • Apache Batik
  • Adobe Flex SDK: swfutils
  • JAI

и начинаем комбинировать.

Перво-наперво вспоминаем про Маркуса и Бориса, и начинаем:

public class PdfConv {
    public int startConversion(String pdfFile) { ... }
    public int getPages() { ... }
    public int nextPage(int pageNo, String outputFileName) { ... }
    public int endConversion() { ... }
}

И пишем как это все будем использовать.

public static void main(String[] argv) throws Exception {
    for(int jjk =0; jjk <argv.length; ++jjk) {
        PdfConv conv = new PdfConv();
        conv.startConversion(argv[jjk]);

        int k = conv.getPages();
        for (int j = 0; j < k; ++j) {
            conv.nextPage(j, argv[jjk] + "_" + j + ".svg");
            conv.nextPage(j, argv[jjk] + "_" + j + ".png");
            conv.nextPage(j, argv[jjk] + "_" + j + ".swf");
            ...
        }
    }
}

Дело за малым — написать все нужные конвертеры.

Первые грабли

image
Выясняется, что выбранная нами либа действительно шустра. Но у нее есть пара серьезных недостатков — нету

  1. SMask
  2. Градиентов
  3. Непонимание JPEG2000
  4. Странное со шрифтами в виде всяких там стрелочек и т.п.

Гугление решает вопросы со шрифтами, добавление JAI в classpath — с форматами изображений.

SMask приходится добавлять напильником в код PDFRenderer. Тривиально — добавляем в парсере распознавание, команду для сохранения в контекст, и отрисовку Shape меняем на рисование в картинку с наложением маски. Банально, но текстообильно.

Градиенты просто игнорируем — их нету в тех местах, которые попадают в слайды. Кропы и прочую обработку я для простоты не показываю, если что.

Первый этап пройден — рисует как надо. Реализуем наш API (обработку ошибок я убрал):

private PDFFile pdf = null;
private FileChannel fic = null;

public int startConversion(String pdfFile)  throws Exception {
    File fix = new File(pdfFile);
    FileInputStream fin = new FileInputStream(fix);
    fic = fin.getChannel();
    MappedByteBuffer mbb = fic.map(FileChannel.MapMode.READ_ONLY, 0, fix.length());
    pdf = new PDFFile(mbb);
    return pdf.getNumPages();
}

public int getPages() throws Exception {
    return pdf.getNumPages();
}

public int endConversion() throws Exception {
    if (fic != null) fic.close();
    pdf = null;
    fic = null;
    return 1;
}

public int nextPage(int pageNo, String outputMask) {
    PDFPage page = pdf.getPage(pageNo + 1);
    if (page == null) return -1;

    Rectangle bounds = page.getBBox().getBounds();
    DrawingCtx ctx = DrawingCtxBuilder.build(
        outputMask, 
        new Dimension(bounds.x + bounds.width, bounds.y + bounds.height));
    PDFRenderer rx = new PDFRenderer(page, ctx.getContext(), bounds, null, null);
    rx.go();
    rx.waitForFinish();
    ctx.saveTo(outputMask);
    return 1;
}

Приступим собственно к конвертации.

Начерчиллим абстракций — но не забудем про рузвельтаты

image
Предметная область такова, что даже Борис не запутается.

abstract class DrawingCtx {
    protected Graphics2D g2;
    protected Dimension size;

    DrawingCtx(Dimension size) { this.size = size; }
    public Graphics2D getGraphics() { return g2; }
    public abstract void saveTo(String fileName) throws Exception;
}

class DrawingCtxBuilder {
    public static build(String fileName, Dimension size) throws Exception {
         String type = fileName.substring(fileName.lastIndexOf('.') + 1).toUpperCase();
         if(type.equals("SVG")) return new SvgDrawingCtx(size);
         else if(type.equals("PNG")) return new ImageDrawingCtx(size);
         else if(type.equals("SWF")) return new SwfDrawingCtx(size);
         ...
         throw new Exception(type + ": unknown converter requested");
    }
}

Наполним мясом наш скелет — и кадаврик будет готов к работе.

SVG и Батик

image
Тут в общем все уже сделано до нас, Apache Batik SVGgen спешит на помощь. Проблем не замечено. Вот были бы градиенты — то тогда да, с радиальными градиентами пришлось бы или распрощаться, или на батик патч накладывать. Это уже после того, как сами градиенты удастся добавить в PDFRenderer, само собой.

Единственная тонкость — хинты правильно составить, чтобы и антиалиасинг не забыть, и картинки на кусочки не порезало.

class SvgDrawingCtx extends DrawingCtx {
    private DOMImplementation domImpl;
    private Document doc;
    private SVGGraphics2D svgGenerator;

    SvgDrawingCtx(Dimension size) {
        super(size);

        domImpl = SVG12DOMImplementation.getDOMImplementation();
        doc = domImpl.createDocument(SVGConstants.SVG_NAMESPACE_URI, SVGConstants.SVG_SVG_TAG, null);
        svgGenerator = new SVGGraphics2D(doc);
        svgGenerator.getGeneratorContext().setPrecision(4);
        svgGenerator.getGeneratorContext().setEmbeddedFontsOn(true);

        g2 = svgGenerator;

        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setRenderingHint(
            RenderingHints.KEY_INTERPOLATION, 
            RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g2.setRenderingHint(
            RenderingHintsKeyExt.KEY_AVOID_TILE_PAINTING, 
            RenderingHintsKeyExt.VALUE_AVOID_TILE_PAINTING_ON);
    }

    public void saveTo(String fn) throws Exception {
         Element svgRoot = svgGenerator.getRoot();
         OutputStream os = new FileOutputStream(fn);
          if (fn.endsWith(".svgz")) os = new GZIPOutputStream(os);
          svgGenerator.stream(svgRoot, new OutputStreamWriter(os), false /* CSS */, true /* escaped */);
          os.close();
    }
}

PNG: проще не бывает

image
Тут настолько банально, что просто приведу код.

class ImageDrawingCtx {
    private BufferedImage bf = null;

    ImageDrawingCtx(Dimension size) {
        super(size);
        bf = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
        g2 = (Graphics2D) bf.getGraphics();
    }

    public void saveTo(String fn) throws Exception {
          OutputStream os = new FileOutputStream(fn);
          ImageIO.write(bf, "PNG", os);
          os.close();
          g2.dispose();
          bf = null;
    }
}

А теперь перейдем к дессерту — создадим SWF

image
Код также прост (и большей частью позаимствован из примеров Flex SDK).

class SwfDrawingCtx extends DrawingCtx {
    SwfDrawingCtx(Dimension size) {
        super(size);
        g2 = new SpriteGraphics2D(size.width, size.height);
    }

    public void saveTo(String fn) throws Exception {
        OutputStream os = new FileOutputStream(fn);
         flash.swf.Frame frame1;
         Movie m = new Movie();
                
         m.version = 7;
         m.bgcolor = new SetBackgroundColor(SwfUtils.colorToInt(255, 255, 255));
         m.framerate = 12;
                
         frame1 = new flash.swf.Frame();
         DefineSprite tag = ((MyG2D) g2).defineSprite("swf-test");
         frame1.controlTags.add(new PlaceObject(tag, 0));
                        
         m.frames = new ArrayList(1);
         m.frames.add(frame1);
                
         TagEncoder tagEncoder = new TagEncoder();
         MovieEncoder movieEncoder = new MovieEncoder(tagEncoder);
         movieEncoder.export(m);
                
         tagEncoder.writeTo(os);
         os.close();
         g2.dispose();
         g2 = null;
     }
}

Ничто не предвещало, и вдруг. Часть картинок в SWF не появилась.

Расследование по горячим следам

image
Вот так вот — в SVG есть, в PNG есть, а в SWF нет.

Трассировка мысленным лучом исходников адоба навела на мысль, и я сделал код конем:

class MyG2D extends SpriteGraphics2D {
    public MyG2D(int width, int height) { super(width, height); }
    public MyG2D() { super(); }
    
    @Override
    public boolean drawImage(Image image, AffineTransform at, ImageObserver obs) {
        // тут много отладки
        return super.drawImage(image, at, obs);
    }
}

Вскрытие показало, что

at.createTransformedShape(new Rectangle(0, 0, image.getWidth(), image.getHeight()).getBounds()

возвращает нам прямоугольник 1x1.

Эврика!

    @Override
    public boolean drawImage(Image image, AffineTransform at, ImageObserver obs) {
        AffineTransform good = getTransform();
        good.concatenate(at);
        return super.drawImage(image, good, obs);
    }

решает проблему.

Итоги

image
Тестовый прогон показал, что первая версия, сшитая на живую нитку, требует меньше 10ти минут. Оно все еще много, и есть над чем подумать.

Однако, переход с кошерного С++ на Java ускорил процедуру более чем в шесть раз, а создание нового конвертера с нуля — потребовало решения пары граблей и в сумме трех дней.

Теперь ребятам есть над чем подумать, и повторить все эти шаги на С++. Время у них есть — java пока справляется.

Издержки: пришлось с собой тянуть 40 мегабайт jar-ок, да и в томкат это дело встраивать, веб сервис однако был на PHP.

Автор: viklequick

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js