package nz.stanger.lecture; import java.awt.Color; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Map; import java.util.Set; import org.apache.batik.anim.dom.SAXSVGDocumentFactory; import org.apache.batik.transcoder.SVGAbstractTranscoder; import org.apache.batik.transcoder.Transcoder; import org.apache.batik.transcoder.TranscoderException; import org.apache.batik.transcoder.TranscoderInput; import org.apache.batik.transcoder.TranscoderOutput; import org.apache.batik.transcoder.image.ImageTranscoder; import org.apache.batik.transcoder.image.PNGTranscoder; import org.apache.batik.util.XMLResourceDescriptor; import org.apache.commons.io.FilenameUtils; import org.apache.fop.activity.ContainerUtil; import org.apache.fop.configuration.Configuration; import org.apache.fop.configuration.ConfigurationException; import org.apache.fop.configuration.DefaultConfigurationBuilder; import org.apache.fop.svg.PDFTranscoder; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileType; import org.gradle.api.tasks.TaskAction; import org.gradle.work.ChangeType; import org.gradle.work.InputChanges; import org.w3c.dom.svg.SVGDocument; import org.w3c.dom.svg.SVGElement; /** * Generate PDF or PNG output from SVG source. * * See {@link https://plantuml.com/api} for details. */ abstract public class GenerateImageFromSvgTask extends GenerateImageFromSourceTask { /** * Files to include in this task. These are Ant-style include and exclude * patterns as per {@code PatternFilterable}. They can be modified by * further calls to {@code include} or {@code exclude}, or replaced entirely * by calling {@code setIncludes}. * * @see org.gradle.api.tasks.util.PatternFilterable */ private static final Set<String> INCLUDE_FILES = Set.of("*.svg"); /** * Maps image formats to corresponding Batik transcoders. */ private static final Map<ImageFormat, Transcoder> TRANSCODERS = Map.of( ImageFormat.PDF, new PDFTranscoder(), ImageFormat.PNG, new PNGTranscoder() ); /** * Constructs a task to generate images from SVG input. */ public GenerateImageFromSvgTask() { DirectoryProperty imageDir = getProject().getExtensions().getByType(LectureExtension.class).getImageDir(); getSource().setDir(imageDir); include(INCLUDE_FILES); getOutputDir().convention(imageDir); getOutputFormat().convention(ImageFormat.PDF); setGroup(Lecture.LECTURE_TASK_GROUP); } /** * Runs this task. * * @param inputChanges the inputs that have changed */ @TaskAction public void runTask(InputChanges inputChanges) { inputChanges.getFileChanges(getSource()).forEach(change -> { if (change.getFileType() != FileType.DIRECTORY && change.getChangeType() != ChangeType.REMOVED) { String inputFilename = change.getNormalizedPath(); String outputFilename = FilenameUtils.removeExtension(inputFilename) + getOutputFormat().get().getFileSuffix(); try { File outputFile = getOutputDir().file(outputFilename).get().getAsFile(); String svgSource = Files.readString(change.getFile().toPath(), StandardCharsets.UTF_8); // https://xmlgraphics.apache.org/batik/using/dom-api.html String parser = XMLResourceDescriptor.getXMLParserClassName(); SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(parser); // https://www.programcreek.com/java-api-examples/?api=org.apache.batik.dom.svg.SAXSVGDocumentFactory SVGDocument svgDocument = factory.createSVGDocument(null, new StringReader(svgSource)); // https://thinktibits.blogspot.com/2012/12/batik-convert-svg-to-pdf-java-example.html // Step 1: read input SVG document into TranscoderInput. TranscoderInput inputSvgImage = new TranscoderInput(svgDocument); // Step 2: define OutputStream to target image and attach to TranscoderOutput ByteArrayOutputStream imageOutputStream = new ByteArrayOutputStream(); TranscoderOutput imageOutput = new TranscoderOutput(imageOutputStream); // Step 3: create appropriate image Transcoder. Transcoder transcoder = TRANSCODERS.get(getOutputFormat().get()); switch (getOutputFormat().get()) { case PNG -> { // The raster images that PlantUML generates look like shit // because they're only 72 DPI, and PlantUML provides no way // to specify higher DPI. The solution is to rasterise the // SVG at higher DPI and scale the width and height accordingly. SVGElement svgRoot = svgDocument.getRootElement(); // The width and height attributes may have units, // most likely "px". Just strip out anything that's // not a number Float scaledWidth = Float.valueOf(svgRoot.getAttribute("width").replaceAll("[^\\d]+", "")) * SCALE_BY_RESOLUTION; Float scaledHeight = Float.valueOf(svgRoot.getAttribute("height").replaceAll("[^\\d]+", "")) * SCALE_BY_RESOLUTION; transcoder.addTranscodingHint(ImageTranscoder.KEY_BACKGROUND_COLOR, Color.WHITE); transcoder.addTranscodingHint(ImageTranscoder.KEY_WIDTH, scaledWidth); transcoder.addTranscodingHint(ImageTranscoder.KEY_HEIGHT, scaledHeight); transcoder.addTranscodingHint(ImageTranscoder.KEY_PIXEL_UNIT_TO_MILLIMETER, PIXEL_UNIT_TO_MM); } case PDF -> { // This seems to be necessary to ensure properly // embedded fonts when generating PDFs. transcoder.addTranscodingHint(SVGAbstractTranscoder.KEY_DEFAULT_FONT_FAMILY, "Open Sans, Helvetica, Verdana, Arial, sans serif"); // See https://stackoverflow.com/q/47884893 and https://stackoverflow.com/a/72807651 DefaultConfigurationBuilder configBuilder = new DefaultConfigurationBuilder(); // See https://stackoverflow.com/a/20389418 Configuration config = configBuilder.build(getClass().getResourceAsStream("/" + FOP_CONFIG_FILENAME)); ContainerUtil.configure((PDFTranscoder) transcoder, config); } default -> throw new TranscoderException("invalid output format: " + getOutputFormat().get().toString()); } // Step 4: write output to target format transcoder.transcode(inputSvgImage, imageOutput); // Step 5: close and flush output stream imageOutputStream.flush(); imageOutputStream.close(); try ( OutputStream fileOutputStream = new FileOutputStream(outputFile);) { imageOutputStream.writeTo(fileOutputStream); } } catch (IOException | TranscoderException | ConfigurationException e) { System.out.println("failed to generate " + outputFilename); // e.printStackTrace(); } } }); } }