Давным давно, когда трава была зеленее,
Как задачу понял архитектор
Дано: есть
Попытка решения: написали плееры на флэше и на 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);
}
}
Вскрытие показало, что
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);
}
решает проблему.
Итоги
Тестовый прогон показал, что первая версия, сшитая на живую нитку, требует меньше 10ти минут. Оно все еще много, и есть над чем подумать.
Однако, переход с кошерного С++ на Java ускорил процедуру более чем в шесть раз, а создание нового конвертера с нуля — потребовало решения пары граблей и в сумме трех дней.
Теперь ребятам есть над чем подумать, и повторить все эти шаги на С++. Время у них есть — java пока справляется.
Издержки: пришлось с собой тянуть 40 мегабайт jar-ок, да и в томкат это дело встраивать, веб сервис однако был на PHP.