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

Резонен вопрос — а чего так сложно-то? Так кроме десктопов мобильники окучить обязательно, а генерировать растр для 200 устройств и потом на всем этом тестить — не наш метод. И потом — а с зумом тогда что делать?
Поэтому для десктопа — флэш в древних браузерах (ура корпоративному IE), и HTML5 для всего остального. Картинки векторные (кроме превьюшек, а то надцать SVG-шек сразу планшеты пока осиливают слабо).
Опен-сорц (как всегда) спешит на помощь.
Анализирую — что же там такое делается. Обнаруживаю вот что
- pdftoswf (ну и что что он discontinued? зато работает)
- pdftosvg (версию 0.1)
- ImageMagick (помним про PNG в виде уменьшенных превьюшек)
- pdftohtml (там ведь есть еще и текст — и надо достать подзаголовки, а из XML это делать как-то удобнее)
Запускаю это все по очереди на тестовом PDF в 500 страниц. Время работы 1 час 2 минуты.
Вот тебе, бабушка, и Юрьев день!
Кто виноват?

Очевидно, что парсить PDF аж четыре раза подряд — не лучший выбор.
Не менее очевидно, что ImageMagick — выбор явно не тот, 3/4 времени потрачено именно утилитой convert.
Именно в этот момент
Общее направление признаем условно верным — куда деваться, релиз как всегда «еще вчера», а к плееру вопросов нет. Но вот утиль считаем подобранным неудачно, и обращаем свое внимание на Java.
Новые действующие лица

Берем следующий набор джентьмена:
- 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"); ... } } }
Дело за малым — написать все нужные конвертеры.
Первые грабли

Выясняется, что выбранная нами либа действительно шустра. Но у нее есть пара серьезных недостатков:
- Отсутствует поддержка SMask
- Градиентов тоже нет
- Непонимание JPEG2000 хроническое
- Странное со шрифтами в виде всяких там стрелочек и т.п. во всей красе
Гугление решает вопросы со шрифтами, добавление 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; }
Приступим собственно к конвертации.
Начерчиллим абстракций — но не забудем про рузвельтаты

Предметная область такова, что даже Борис не запутается.
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 и Батик

Тут в общем все уже сделано до нас, 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: проще не бывает

Тут настолько банально, что просто приведу код.
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

Код также прост (и большей частью позаимствован из примеров 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 не появилась.
Расследование по горячим следам

Вот так вот — в 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); } }
Вскрытие показало, что
возвращает нам прямоугольник 1x1.at.createTransformedShape(new Rectangle(0, 0, image.getWidth(), image.getHeight()).getBounds()
Эврика!
@Override public boolean drawImage(Image image, AffineTransform at, ImageObserver obs) { AffineTransform good = getTransform(); good.concatenate(at); return super.drawImage(image, good, obs); }
решает проблему.
Итоги

Тестовый прогон показал, что первая версия, сшитая на живую нитку, требует меньше 10ти минут. Оно все еще много, и есть над чем подумать.
Однако, переход с кошерного С++ на Java ускорил процедуру более чем в шесть раз, а создание нового конвертера с нуля — потребовало решения пары граблей и в сумме трех дней.
Теперь ребятам есть над чем подумать, и повторить все эти шаги на С++. Время у них есть — java пока справляется.
Издержки: пришлось с собой тянуть 40 мегабайт jar-ок, да и в томкат это дело встраивать, веб сервис однако был на PHP.
