Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: fc145b9e2c7b22f8b417a00bf5deaec3b6c30c76 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
/*******************************************************************************
 * (c) Copyright 2013 l33t labs LLC and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     l33t labs LLC and others - initial contribution 
 *******************************************************************************/

package org.eclipse.ui.images.renderer;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

import javax.imageio.ImageIO;

import org.apache.batik.dom.svg.SAXSVGDocumentFactory;
import org.apache.batik.transcoder.ErrorHandler;
import org.apache.batik.transcoder.TranscoderException;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.PNGTranscoder;
import org.apache.batik.util.XMLResourceDescriptor;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.w3c.dom.Element;
import org.w3c.dom.svg.SVGDocument;

import com.jhlabs.image.GrayscaleFilter;
import com.jhlabs.image.HSBAdjustFilter;
import com.mortennobel.imagescaling.ResampleFilters;
import com.mortennobel.imagescaling.ResampleOp;

/**
 * <p>Mojo which renders SVG icons into PNG format./p>
 * 
 * @goal render-icons
 * @phase generate-resources
 */
public class RenderMojo extends AbstractMojo {

    /** Maven logger */
    Log log;

    /** Used for high res rendering support. */
    public static final String ECLIPSE_SVG_HIGHRES = "eclipse.svg.highres";

    /** Used to specify the number of render threads when rasterizing icons. */
    public static final String RENDERTHREADS = "eclipse.svg.renderthreads";

    /**
     * <p>IconEntry is used to define an icon to rasterize,
     * where to put it and the dimensions to render it at.</p>
     */
    class IconEntry {

        /** The name of the icon minus extension */
        String nameBase;

        /** The input path of the source svg files. */
        File inputPath;

        /**
         * The path rasterized versions of this icon should be written into.
         */
        File outputPath;

        /** The path to a disabled version of the icon (gets desaturated). */
        private File disabledPath;

        /**
         * Creates an IconEntry used for record keeping when
         * rendering a set of SVG icons.
         * 
         * @param nameBase the name of the icon file, minus any extension
         * @param inputPath the SVG file that is rendered
         * @param outputPath the path to the rendered icon data
         * @param disabledPath the part to the disabled version of the output icon
         */
        public IconEntry(String nameBase, File inputPath, File outputPath, 
                File disabledPath) {
            this.nameBase = nameBase;
            this.inputPath = inputPath;
            this.outputPath = outputPath;
            this.disabledPath = disabledPath;
        }
    }

    /** A list of directories with svg sources to rasterize. */
    private List<IconEntry> icons;

    /** The pool used to render multiple icons concurrently. */
    private ExecutorService execPool;

    /** The number of threads to use when rendering icons. */
    private int threads;

    /**
     * A counter used to keep track of the number of rendered icons. Atomic is
     * used to make it easy to access between threads concurrently.
     */
    private AtomicInteger counter;

    /**
     * A collection of lists for each Eclipse icon sets (o.e.workbench.ui,
     * o.e.jd.ui, etc).
     */
    Map<String, List<IconEntry>> galleryIconSets;

    /** List of icons that failed to render, made safe for parallel access */
    List<IconEntry> failedIcons = Collections
            .synchronizedList(new ArrayList<IconEntry>(5));

    /** Whether to render the icons at 2x for high dpi displays. */
    private boolean highres;

    /** Used for creating desaturated icons */
    private GrayscaleFilter grayFilter;

    /** Used for creating desaturated icons */
    private HSBAdjustFilter desaturator;

    /**
     * @return the number of icons rendered at the time of the call
     */
    public int getIconsRendered() {
        return counter.get();
    }
    
    /**
     * @return the number of icons that failed during the rendering process
     */
    public int getFailedIcons() {
        return failedIcons.size();
    }

    /**
     * <p>Creates an IconEntry during the icon gather operation.</p>
     * 
     * @param input the source of the icon file (SVG document)
     * @param outputPath the path of the rasterized version to generate
     * @param disabledPath the path of the disabled (desaturated) icon, if one is required
     * 
     * @return an IconEntry describing the rendering operation
     */
    public IconEntry createIcon(File input, File outputPath, File disabledPath) {
        String name = input.getName();
        String[] split = name.split("\\.(?=[^\\.]+$)");

        IconEntry def = new IconEntry(split[0], input, outputPath, disabledPath);

        return def;
    }

    /**
     * <p>Generates raster images from the input SVG vector image.</p>
     * 
     * @param icon
     *            the icon to render
     */
    public void rasterize(IconEntry icon) {
        if (icon == null) {
            log.error("Null icon definition, skipping.");
            failedIcons.add(icon);
            return;
        }

        if (icon.inputPath == null) {
            log.error("Null icon input path, skipping: "
                    + icon.nameBase);
            failedIcons.add(icon);
            return;
        }

        if (!icon.inputPath.exists()) {
            log.error("Input path specified does not exist, skipping: "
                            + icon.nameBase);
            failedIcons.add(icon);
            return;
        }

        if (icon.outputPath != null && !icon.outputPath.exists()) {
            icon.outputPath.mkdirs();
        }

        if (icon.disabledPath != null && !icon.disabledPath.exists()) {
            icon.disabledPath.mkdirs();
        }

        // Create the document to rasterize
        SVGDocument svgDocument = generateSVGDocument(icon);
        
        if(svgDocument == null) {
            return;
        }

        // Determine the output sizes (native, double, quad)
        // We render at quad size and resample down for output
        Element svgDocumentNode = svgDocument.getDocumentElement();
        String nativeWidthStr = svgDocumentNode.getAttribute("width");
        String nativeHeightStr = svgDocumentNode.getAttribute("height");

        int nativeWidth = Integer.parseInt(nativeWidthStr);
        int nativeHeight = Integer.parseInt(nativeHeightStr);

        int doubleWidth = nativeWidth * 2;
        int doubleHeight = nativeHeight * 2;

        int quadWidth = nativeWidth * 4;
        int quadHeight = nativeHeight * 4;

        // Guesstimate the PNG size in memory, BAOS will enlarge if necessary.
        int outputInitSize = quadWidth * quadHeight * 4 + 1024;
        ByteArrayOutputStream iconOutput = new ByteArrayOutputStream(
                outputInitSize);

        // Render to SVG
        try {
            log.info(Thread.currentThread().getName() + " "
                    + " Rasterizing: " + icon.nameBase + ".png at " + quadWidth
                    + "x" + quadHeight);
            
            TranscoderInput svgInput = new TranscoderInput(svgDocument);
            
            boolean success = renderIcon(icon.nameBase, quadWidth, quadHeight, svgInput, iconOutput);
            
            if (!success) {
                log.error("Failed to render icon: " + icon.nameBase + ".png, skipping.");
                failedIcons.add(icon);
                return;
            }
        } catch (Exception e) {
            log.error("Failed to render icon: " + e.getMessage());
            failedIcons.add(icon);
            return;
        }

        // Generate a buffered image from Batik's png output
        byte[] imageBytes = iconOutput.toByteArray();
        ByteArrayInputStream imageInputStream = new ByteArrayInputStream(imageBytes);
        
        BufferedImage inputImage = null;
        try {
            inputImage = ImageIO.read(imageInputStream);
            
            if(inputImage == null) {
                log.error("Failed to generate BufferedImage from rendered icon, ImageIO returned null: " + icon.nameBase);
                failedIcons.add(icon);
                return;
            }
        } catch (IOException e2) {
            log.error("Failed to generate BufferedImage from rendered icon: "  + icon.nameBase + " - " + e2.getMessage());
            failedIcons.add(icon);
            return;
        }

        // Icons lose definition and accuracy when rendered directly
        // to <128px res with Batik
        // Here we resize a 16x,32x,48x,64x images down, which gives better
        // results
        
        // Default to the native svg size
        int targetWidth = nativeWidth;
        int targetHeight = nativeHeight;

        if(!highres) {
            log.info(Thread.currentThread().getName() + " "
                    + " Rasterizing (Scaling Native): " + icon.nameBase
                    + ".png at " + nativeWidth + "x" + nativeHeight);
        } else {
            log.info(Thread.currentThread().getName() + " "
                    + " Rasterizing (Scaling Half): " + icon.nameBase
                    + ".png at " + doubleWidth + "x" + doubleWidth);
            
            targetWidth = doubleWidth;
            targetHeight = doubleHeight;
        }
        
        resizeIcon(icon, targetWidth, targetHeight, inputImage);
    }

    /**
     * <p>Generates a Batik SVGDocument for the supplied IconEntry's input
     * file.</p>
     * 
     * @param icon the icon entry to generate an SVG document for
     * 
     * @return a batik SVGDocument instance or null if one could not be generated
     */
    private SVGDocument generateSVGDocument(IconEntry icon) {
        // Load the document and find out the native height/width
        // We reuse the document later for rasterization
        SVGDocument svgDocument = null;
        try {
            FileInputStream iconDocumentStream = new FileInputStream(icon.inputPath);

            String parser = XMLResourceDescriptor.getXMLParserClassName();
            SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);
            
            // What kind of URI is batik expecting here??? the docs don't say
            svgDocument = f.createSVGDocument("file://" + icon.nameBase + ".svg", iconDocumentStream);
        } catch (Exception e3) {
            log.error("Error parsing SVG icon document: " + e3.getMessage());
            failedIcons.add(icon);
            return null;
        }
        return svgDocument;
    }

    /**
     * <p>Resizes the supplied inputImage to the specified width and height, using
     * lanczos resampling techniques.</p>
     *  
     * @param icon the icon that's being resized
     * @param width the desired output width after rescaling operations
     * @param height the desired output height after rescaling operations
     * @param sourceImage the source image to resource
     */
    private void resizeIcon(IconEntry icon, int width, int height, BufferedImage sourceImage) {
        ResampleOp resampleOpNative = new ResampleOp(width, height);
        resampleOpNative.setFilter(ResampleFilters.getLanczos3Filter());
        // resampleOp.setUnsharpenMask(AdvancedResizeOp.UnsharpenMask.Oversharpened);
        resampleOpNative.setNumberOfThreads(2);
        
        try {
            // Resize and render the 16x16 icon
            BufferedImage rescaled = resampleOpNative.filter(sourceImage, null);

            ImageIO.write(rescaled, "PNG", new File(icon.outputPath, icon.nameBase + ".png"));
            
            if (icon.disabledPath != null) {
                BufferedImage desaturated16 = desaturator.filter(
                        grayFilter.filter(rescaled, null), null);

                ImageIO.write(desaturated16, "PNG", new File(icon.disabledPath, icon.nameBase + ".png"));
            }
        } catch (Exception e1) {
            log.error("Failed to resize rendered icon to output size: "  + 
                               icon.nameBase + " - " + e1.getMessage());
            failedIcons.add(icon);
        }
    }

    /**
     * <p>Handles concurrently rasterizing the icons to
     * reduce the duration on multicore systems.</p>
     */
    public void rasterizeAll() {
        // The number of icons that haven't been distributed to
        // callables
        int remainingIcons = icons.size();
        
        // The number of icons to distribute to a rendering callable
        final int threadExecSize = icons.size() / this.threads;

        // The current offset to start a batch, as they're distributed
        // between rendering callables
        int batchOffset = 0;

        // A list of callables used to render icons on multiple threads
        // Each callable gets a set of icons to render
        List<Callable<Object>> tasks = new ArrayList<Callable<Object>>(
                this.threads);

        // Distribute the rasterization operations between multiple threads
        while (remainingIcons > 0) {
            // The current start index for the current batch
            final int batchStart = batchOffset;
            
            // Increment the offset to reflect this batch (used for the next batch)
            batchOffset += threadExecSize;

            // Determine this batch size, used for batches that have less than
            // threadExecSize at the end of the distribution operation
            int batchSize = 0;

            // Determine if we can fit a full batch in this callable
            // or if we are at the end of gathered icons
            if (remainingIcons > threadExecSize) {
                batchSize = threadExecSize;
            } else {
                // We have less than a full batch worth of remaining icons
                // just add them all
                batchSize = remainingIcons;
            }

            // Deincrement the remaining Icons
            remainingIcons -= threadExecSize;

            // Used for access in the callable's scope
            final int execCount = batchSize;

            // Create the callable and add it to the task pool
            Callable<Object> runnable = new Callable<Object>() {
                public Object call() throws Exception {
                    // Rasterize this batch
                    for (int count = 0; count < execCount; count++) {
                        rasterize(icons.get(batchStart + count));
                    }

                    // Update the render counter
                    counter.getAndAdd(execCount);
                    log.info("Finished rendering batch, index: " + batchStart);

                    return null;
                }
            };

            tasks.add(runnable);
        }

        // Execute the rasterization operations that
        // have been added to the pool
        try {
            execPool.invokeAll(tasks);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        // Print info about failed render operations, so they can be fixed
        log.info("Failed Icon Count: " + failedIcons.size());
        for (IconEntry icon : failedIcons) {
            log.info("Failed Icon: " + icon.nameBase);
        }

    }

    /**
     * Use batik to rasterize the input SVG into a raster image at the specified
     * image dimensions.
     * 
     * @param width the width to render the icons at
     * @param height the height to render the icon at
     * @param input the SVG transcoder input
     * @param stream the stream to write the PNG data to
     */
    public boolean renderIcon(final String iconName, int width, int height,
            TranscoderInput tinput, OutputStream stream) {
        PNGTranscoder t = new PNGTranscoder();
        t.addTranscodingHint(PNGTranscoder.KEY_WIDTH, new Float(width));
        t.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, new Float(height));

        t.setErrorHandler(new ErrorHandler() {
            public void warning(TranscoderException arg0)
                    throws TranscoderException {
                log.error("Icon: " + iconName + " - WARN: " + arg0.getMessage());
            }

            public void fatalError(TranscoderException arg0)
                    throws TranscoderException {
                log.error("Icon: " + iconName + " - FATAL: " + arg0.getMessage());
            }

            public void error(TranscoderException arg0)
                    throws TranscoderException {
                log.error("Icon: " + iconName + " - ERROR: " + arg0.getMessage());
            }
        });

        // Transcode the SVG document input to a PNG via the output stream
        TranscoderOutput output = new TranscoderOutput(stream);

        try {
            t.transcode(tinput, output);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            try {
                stream.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    /**
     * <p>Search the root resources directory for svg icons and add them to our
     * collection for rasterization later.</p>
     * 
     * @param outputName
     * @param iconDir
     * @param outputBase
     * @param outputDir2
     */
    public void gatherIcons(String outputName, File rootDir, File iconDir,
            File outputBase) {

        File[] listFiles = iconDir.listFiles();

        for (File child : listFiles) {
            if (child.isDirectory()) {
                gatherIcons(outputName, rootDir, child, outputBase);
                continue;
            }

            if (!child.getName().endsWith("svg")) {
                return;
            }
        
            // Compute a relative path for the output dir
            URI rootUri = rootDir.toURI();
            URI iconUri = iconDir.toURI();

            String relativePath = rootUri.relativize(iconUri).getPath();
            File outputDir = new File(outputBase, relativePath);
            File disabledOutputDir = null;

            File parentFile = child.getParentFile();

            /* Determine if/where to put a disabled version of the icon
               Eclipse traditionally uses a prefix of d for disabled, e for
               enabled in the folder name */
            if (parentFile != null) {
                String parentDirName = parentFile.getName();
                if (parentDirName.startsWith("e")) {
                    StringBuilder builder = new StringBuilder();
                    builder.append("d");
                    builder.append(parentDirName.substring(1,
                            parentDirName.length()));
                    String disabledVariant = builder.toString();

                    File setParent = parentFile.getParentFile();
                    for (File disabledFolder : setParent.listFiles()) {
                        if (disabledFolder.getName()
                                .equals(disabledVariant)) {
                            String path = rootUri.relativize(
                                    disabledFolder.toURI()).getPath();
                            disabledOutputDir = new File(outputBase, path);
                        }
                    }

                }
            }

            IconEntry icon = createIcon(child, outputDir, disabledOutputDir);
            
            icons.add(icon);
        }
    }

    /**
     * <p>Initializes rasterizer defaults</p>
     * 
     * @param threads the number of threads to render with
     * @param highres whether to render high res output
     */
    private void init(int threads, boolean highres) {
        this.threads = threads;
        this.highres = highres;
        icons = new ArrayList<IconEntry>();
        execPool = Executors.newFixedThreadPool(threads);
        counter = new AtomicInteger();
        
        grayFilter = new GrayscaleFilter();

        desaturator = new HSBAdjustFilter();
        desaturator.setSFactor(0.0f);
    }
    
    /**
     * @see AbstractMojo#execute()
     */
    public void execute() throws MojoExecutionException, MojoFailureException {
        log = getLog();
        
        // Default to 2x the number of processor cores but allow override via jvm arg
        int threads = Math.max(1, Runtime.getRuntime().availableProcessors() * 2);
        String threadStr = System.getProperty(RENDERTHREADS);
        if (threadStr != null) {
            try {
                threads = Integer.parseInt(threadStr);
            } catch (Exception e) {
                e.printStackTrace();
                System.out
                        .println("Could not parse thread count, using default thread count");
            }
        }
        
        // if high res is enabled, the icons will be rendered at 2x their native svg size
        String highresStr = System.getProperty(ECLIPSE_SVG_HIGHRES);
        boolean highres = Boolean.parseBoolean(highresStr);
        
        // Track the time it takes to render the entire set
        long totalStartTime = System.currentTimeMillis();
        
        // initialize defaults (the old renderer was instantiated via constructor)
        init(threads, highres);

        String workingDirectory = System.getProperty("user.dir");
        
        File outputDir = new File(workingDirectory+"/eclipse-png/");
        File iconDirectoryRoot = new File("eclipse-svg/");

        // Search each subdir in the root dir for svg icons
        for (File file : iconDirectoryRoot.listFiles()) {
            if(!file.isDirectory()) {
                continue;
            }
            
            String dirName = file.getName();
            
            // Where to place the rendered icon
            File outputBase = new File(outputDir, dirName);

            gatherIcons(dirName, file, file, outputBase);
        }
        
        log.info("Working directory: " + outputDir.getAbsolutePath());
        log.info("SVG Icon Directory: " + iconDirectoryRoot.getAbsolutePath());
        log.info("Rendering icons with " + threads + " threads, high res output enabled: " + highres);
        long startTime = System.currentTimeMillis();
        
        // Render the icons
        rasterizeAll();

        // Print summary of operations
        int iconRendered = getIconsRendered();
        int failedIcons = getFailedIcons();
        int fullIconCount = iconRendered - failedIcons;
        
        log.info(fullIconCount + " Icons Rendered");
        log.info(failedIcons + " Icons Failed");
        log.info("Took: "    + (System.currentTimeMillis() - startTime) + " ms.");

        log.info("Rasterization operations completed, Took: "
                + (System.currentTimeMillis() - totalStartTime) + " ms.");
    }

}

Back to the top