diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PngCreator.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PngCreator.java new file mode 100755 index 0000000000000000000000000000000000000000..1ba895d140d5de280203ae4ab73fadf8874cb925 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PngCreator.java @@ -0,0 +1,23 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * <p> + * Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). + * <p> + * This program is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation; either version 3.0 of the License, or (at your option) any later + * version. + * <p> + * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * <p> + * You should have received a copy of the GNU Lesser General Public License along + * with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. + */ + +package org.bigbluebutton.presentation; + +public interface PngCreator { + public boolean createPng(UploadedPresentation pres); +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java index e645015a536951ac8bb2d4e882a3cfa665f710df..8901806c0bb6e8a0f545f24df3066e0f014a5f2b 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java @@ -33,15 +33,8 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.bigbluebutton.presentation.ConversionMessageConstants; -import org.bigbluebutton.presentation.ConversionUpdateMessage; +import org.bigbluebutton.presentation.*; import org.bigbluebutton.presentation.ConversionUpdateMessage.MessageBuilder; -import org.bigbluebutton.presentation.PageConverter; -import org.bigbluebutton.presentation.PdfToSwfSlide; -import org.bigbluebutton.presentation.SvgImageCreator; -import org.bigbluebutton.presentation.TextFileCreator; -import org.bigbluebutton.presentation.ThumbnailCreator; -import org.bigbluebutton.presentation.UploadedPresentation; import org.bigbluebutton.presentation.messages.DocPageCountExceeded; import org.bigbluebutton.presentation.messages.DocPageCountFailed; import org.slf4j.Logger; @@ -57,12 +50,16 @@ public class PdfToSwfSlidesGenerationService { private PageConverter pdfToSwfConverter; private ExecutorService executor; private ThumbnailCreator thumbnailCreator; + private PngCreator pngCreator; + private TextFileCreator textFileCreator; private SvgImageCreator svgImageCreator; private long MAX_CONVERSION_TIME = 5 * 60 * 1000; private String BLANK_SLIDE; private int MAX_SWF_FILE_SIZE; private boolean svgImagesRequired; + private boolean generatePngs; + private final long CONVERSION_TIMEOUT = 20000000000L; // 20s public PdfToSwfSlidesGenerationService(int numConversionThreads) { @@ -81,6 +78,11 @@ public class PdfToSwfSlidesGenerationService { createSvgImages(pres); } + // only create PNG images if the configuration requires it + if (generatePngs) { + createPngImages(pres); + } + notifier.sendConversionCompletedMessage(pres); } } @@ -161,6 +163,10 @@ public class PdfToSwfSlidesGenerationService { svgImageCreator.createSvgImages(pres); } + private void createPngImages(UploadedPresentation pres) { + pngCreator.createPng(pres); + } + private void convertPdfToSwf(UploadedPresentation pres) { int numPages = pres.getNumberOfPages(); List<PdfToSwfSlide> slides = setupSlides(pres, numPages); @@ -318,14 +324,22 @@ public class PdfToSwfSlidesGenerationService { this.MAX_SWF_FILE_SIZE = size; } - public void setSvgImagesRequired(boolean svg) { - this.svgImagesRequired = svg; + public void setGeneratePngs(boolean generatePngs) { + this.generatePngs = generatePngs; } + public void setSvgImagesRequired(boolean svg) { + this.svgImagesRequired = svg; + } + public void setThumbnailCreator(ThumbnailCreator thumbnailCreator) { this.thumbnailCreator = thumbnailCreator; } + public void setPngCreator(PngCreator pngCreator) { + this.pngCreator = pngCreator; + } + public void setTextFileCreator(TextFileCreator textFileCreator) { this.textFileCreator = textFileCreator; } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PngCreatorImp.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PngCreatorImp.java new file mode 100755 index 0000000000000000000000000000000000000000..9ae9c14825c4fa51933ed99110752bc6836fd761 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PngCreatorImp.java @@ -0,0 +1,173 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * <p> + * Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below). + * <p> + * This program is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation; either version 3.0 of the License, or (at your option) any later + * version. + * <p> + * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * <p> + * You should have received a copy of the GNU Lesser General Public License along + * with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. + */ + +package org.bigbluebutton.presentation.imp; + +import com.google.gson.Gson; +import org.apache.commons.io.FileUtils; +import org.bigbluebutton.presentation.PngCreator; +import org.bigbluebutton.presentation.UploadedPresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class PngCreatorImp implements PngCreator { + private static Logger log = LoggerFactory.getLogger(PngCreatorImp.class); + + private static final Pattern PAGE_NUMBER_PATTERN = Pattern.compile("(.+-png)-([0-9]+)(.png)"); + + private String BLANK_PNG; + private int slideWidth = 800; + + private static String TEMP_PNG_NAME = "temp-png"; + + public boolean createPng(UploadedPresentation pres) { + boolean success = false; + File pngDir = determinePngDirectory(pres.getUploadedFile()); + + if (!pngDir.exists()) + pngDir.mkdir(); + + cleanDirectory(pngDir); + + try { + success = generatePngs(pngDir, pres); + } catch (InterruptedException e) { + log.warn("Interrupted Exception while generating png."); + success = false; + } + + // Create blank thumbnails for pages that failed to generate a thumbnail. + createBlankPngs(pngDir, pres.getNumberOfPages()); + + renamePng(pngDir); + + return success; + } + + private boolean generatePngs(File pngsDir, UploadedPresentation pres) + throws InterruptedException { + String source = pres.getUploadedFile().getAbsolutePath(); + String dest; + String COMMAND = ""; + dest = pngsDir.getAbsolutePath() + File.separator + TEMP_PNG_NAME; + COMMAND = "pdftocairo -png -scale-to " + slideWidth + " " + source + " " + dest; + + boolean done = new ExternalProcessExecutor().exec(COMMAND, 60000); + + if (done) { + return true; + } else { + Map<String, Object> logData = new HashMap<String, Object>(); + logData.put("meetingId", pres.getMeetingId()); + logData.put("presId", pres.getId()); + logData.put("filename", pres.getName()); + logData.put("message", "Failed to create png."); + + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + log.warn("-- analytics -- " + logStr); + } + + return false; + } + + private File determinePngDirectory(File presentationFile) { + return new File(presentationFile.getParent() + File.separatorChar + "pngs"); + } + + private void renamePng(File dir) { + /* + * If more than 1 file, filename like 'temp-png-X.png' else filename is + * 'temp-png.png' + */ + if (dir.list().length > 1) { + File[] files = dir.listFiles(); + Matcher matcher; + for (int i = 0; i < files.length; i++) { + matcher = PAGE_NUMBER_PATTERN.matcher(files[i].getAbsolutePath()); + if (matcher.matches()) { + // Path should be something like + // 'c:/temp/bigluebutton/presname/pngs/temp-png-1.png' + // Extract the page number. There should be 4 matches. + // 0. c:/temp/bigluebutton/presname/pngs/temp-png-1.png + // 1. c:/temp/bigluebutton/presname/pngs/temp-png + // 2. 1 ---> what we are interested in + // 3. .png + // We are interested in the second match. + int pageNum = Integer.valueOf(matcher.group(2).trim()).intValue(); + String newFilename = "slide-" + (pageNum) + ".png"; + File renamedFile = new File( + dir.getAbsolutePath() + File.separator + newFilename); + files[i].renameTo(renamedFile); + } + } + } else if (dir.list().length == 1) { + File oldFilename = new File( + dir.getAbsolutePath() + File.separator + dir.list()[0]); + String newFilename = "slide-1.png"; + File renamedFile = new File( + oldFilename.getParent() + File.separator + newFilename); + oldFilename.renameTo(renamedFile); + } + } + + private void createBlankPngs(File pngsDir, int pageCount) { + File[] pngs = pngsDir.listFiles(); + + if (pngs.length != pageCount) { + for (int i = 0; i < pageCount; i++) { + File png = new File(pngsDir.getAbsolutePath() + File.separator + TEMP_PNG_NAME + "-" + i + ".png"); + if (!png.exists()) { + log.info("Copying blank png for slide " + i); + copyBlankPng(png); + } + } + } + } + + private void copyBlankPng(File png) { + try { + FileUtils.copyFile(new File(BLANK_PNG), png); + } catch (IOException e) { + log.error("IOException while copying blank thumbnail."); + } + } + + private void cleanDirectory(File directory) { + File[] files = directory.listFiles(); + for (int i = 0; i < files.length; i++) { + files[i].delete(); + } + } + + public void setBlankPng(String blankPng) { + BLANK_PNG = blankPng; + } + + public void setSlideWidth(int width) { + slideWidth = width; + } + +} diff --git a/bigbluebutton-web/grails-app/conf/UrlMappings.groovy b/bigbluebutton-web/grails-app/conf/UrlMappings.groovy index ef1cf7959287072a346f42b2264c9ec9353a5441..e7d2eba07111ebe601b1bf67efeef393c45c22c0 100755 --- a/bigbluebutton-web/grails-app/conf/UrlMappings.groovy +++ b/bigbluebutton-web/grails-app/conf/UrlMappings.groovy @@ -29,6 +29,10 @@ class UrlMappings { action = [GET:'showThumbnail'] } + "/presentation/$conference/$room/$presentation_name/png/$id"(controller:"presentation") { + action = [GET:'showPng'] + } + "/presentation/$conference/$room/$presentation_name/svgs"(controller:"presentation") { action = [GET:'numberOfSvgs'] } diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index 6916bdb3e94b667cfb1baae585fff30ee9839a14..4bb688af3f495dfbe703f22ec49e50425f5c8308 100755 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -50,6 +50,7 @@ presCheckExec=/usr/share/prescheck/prescheck.sh BLANK_SLIDE=/var/bigbluebutton/blank/blank-slide.swf BLANK_PRESENTATION=/var/bigbluebutton/blank/blank-presentation.pdf BLANK_THUMBNAIL=/var/bigbluebutton/blank/blank-thumb.png +BLANK_PNG=/var/bigbluebutton/blank/blank-png.png #---------------------------------------------------- # Number of minutes the conversion should take. If it takes @@ -88,6 +89,12 @@ numConversionThreads=2 # to be used in the HTML5 client svgImagesRequired=false +#---------------------------------------------------- +# Additional conversion of the presentation slides to PNG +# to be used in the IOS mobile client +generatePngs=true +pngSlideWidth=1200 + # Default number of digits for voice conference users joining through the PSTN. defaultNumDigitsForTelVoice=5 diff --git a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml index 1a053f0b7def608749d8b9a2c52c9da87be011a6..d649f48bc10482f7bf567360e917f9c990eb3c79 100755 --- a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml +++ b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml @@ -70,6 +70,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <property name="blankThumbnail" value="${BLANK_THUMBNAIL}"/> </bean> + <bean id="pngCreator" class="org.bigbluebutton.presentation.imp.PngCreatorImp"> + <property name="blankPng" value="${BLANK_PNG}"/> + <property name="slideWidth" value="${pngSlideWidth}"/> + </bean> + <bean id="textFileCreator" class="org.bigbluebutton.presentation.imp.TextFileCreatorImp"/> <bean id="svgImageCreator" class="org.bigbluebutton.presentation.imp.SvgImageCreatorImp"> @@ -84,6 +89,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <property name="counterService" ref="pageCounterService"/> <property name="pageConverter" ref="pdf2SwfPageConverter"/> <property name="thumbnailCreator" ref="thumbCreator"/> + <property name="pngCreator" ref="pngCreator"/> <property name="textFileCreator" ref="textFileCreator"/> <property name="svgImageCreator" ref="svgImageCreator"/> <property name="blankSlide" value="${BLANK_SLIDE}"/> @@ -91,6 +97,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <property name="maxConversionTime" value="${maxConversionTime}"/> <property name="swfSlidesGenerationProgressNotifier" ref="swfSlidesGenerationProgressNotifier"/> <property name="svgImagesRequired" value="${svgImagesRequired}"/> + <property name="generatePngs" value="${generatePngs}"/> </bean> <bean id="imageToSwfSlidesGenerationService" diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy index d47df65d9a9f564fe248740342c589db52104906..69ae7a25a41934b83a4450b6940a4431993f9d36 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy @@ -224,6 +224,29 @@ class PresentationController { return null; } + + def showPng = { + def presentationName = params.presentation_name + def conf = params.conference + def rm = params.room + def png = params.id + + InputStream is = null; + try { + def pres = presentationService.showPng(conf, rm, presentationName, png) + if (pres.exists()) { + + def bytes = pres.readBytes() + response.addHeader("Cache-Control", "no-cache") + response.contentType = 'image' + response.outputStream << bytes; + } + } catch (IOException e) { + log.error("Error reading file.\n" + e.getMessage()); + } + + return null; + } def showTextfile = { def presentationName = params.presentation_name @@ -340,7 +363,7 @@ class PresentationController { } } } - } + } } def numberOfSvgs = { diff --git a/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy b/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy index 1f45e8f1e49e160377f06980a6067b6c3041a018..88f765f9901ead73c5cd10221b5a9097a614361a 100755 --- a/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy +++ b/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy @@ -109,6 +109,14 @@ class PresentationService { new File(thumbFile) } + def showPng = {conf, room, presentationName, page -> + def pngFile = roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + + "pngs" + File.separatorChar + "slide-${page}.png" + log.debug "showing $pngFile" + + new File(pngFile) + } + def showTextfile = {conf, room, presentationName, textfile -> def txt = roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + "textfiles" + File.separatorChar + "slide-${textfile}.txt"