diff --git a/components/ILIAS/Certificate/classes/File/Template/XLS/class.ilXlsFoParser.php b/components/ILIAS/Certificate/classes/File/Template/XLS/class.ilXlsFoParser.php index 8a12479c7b94..e7e0efd5fc1e 100755 --- a/components/ILIAS/Certificate/classes/File/Template/XLS/class.ilXlsFoParser.php +++ b/components/ILIAS/Certificate/classes/File/Template/XLS/class.ilXlsFoParser.php @@ -83,8 +83,8 @@ public function parse(array $formData): string $xsl = $this->certificateXlsFileLoader->getXlsCertificateContent(); // additional font support - $xsl = str_replace( - 'font-family="Helvetica, unifont"', + $xsl = preg_replace( + '/font-family="[^"]*"/', 'font-family="' . $this->settings->get('rpc_pdf_font', 'Helvetica, unifont') . '"', $xsl ); diff --git a/components/ILIAS/WebServices/RPC/lib/README.md b/components/ILIAS/WebServices/RPC/lib/README.md index 6edc7f69cca3..ecc8654db8b0 100755 --- a/components/ILIAS/WebServices/RPC/lib/README.md +++ b/components/ILIAS/WebServices/RPC/lib/README.md @@ -77,6 +77,9 @@ NumThreads = 2 RamBufferSize = 256 IndexMaxFileSizeMB = 500 +[FOP] +FontDirectory = /var/www/files/fonts + [Client1] ClientId = ACMECorp NicId = 0 @@ -93,6 +96,8 @@ IliasIniPath = /var/www/html/ilias/ilias.ini.php - RamBufferSize: The maximum amount of memory in MB before index data is written to the file system - IndexMaxFileSize: The maximum file size of ILIAS files that can be included in the index. +- FontDirectory: An optional directory containing additional fonts besides "Helvetica (embedded)" and "unifont" for the FOP PDF renderer. + - ClientId: ClientId of ILIAS installation - NicId: NicID of ILIAS installation - IliasMajorVersion: the ILIAS major version the client is based on diff --git a/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/settings/CommonsIniFileParser.java b/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/settings/CommonsIniFileParser.java index 5e3d8a4a21ee..e8532dc92563 100755 --- a/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/settings/CommonsIniFileParser.java +++ b/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/settings/CommonsIniFileParser.java @@ -88,6 +88,18 @@ public void parseSettings(String path, boolean parseClientSettings) throws Confi serverSettings.setMaxFileSizeMB(getConfig(ini, section, "IndexMaxFileSizeMB", false)); } } + if (section.equals("FOP")) { + logger.debug("Parsing FOP settings"); + if (sectionConfig.containsKey("FontDirectory")) { + logger.debug("FontDirectory {}", getConfig(ini, section, "FontDirectory", false)); + try { + serverSettings.setFopFontDirectory(getConfig(ini, section, "FontDirectory", false)); + } catch (ConfigurationException e) { + logger.warn("Ignoring invalid configuration option FOP FontDirectory: {}", e); + } + } + } + logger.debug("check section client"); if (section.startsWith("Client") && parseClientSettings) { logger.debug("section client"); diff --git a/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/settings/ServerSettings.java b/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/settings/ServerSettings.java index e48d34a23be4..62fdd09f7bfa 100755 --- a/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/settings/ServerSettings.java +++ b/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/settings/ServerSettings.java @@ -23,263 +23,308 @@ package de.ilias.services.settings; import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.awt.*; import java.io.File; import java.io.IOException; import java.net.InetAddress; +import java.net.URI; import java.net.UnknownHostException; +import java.nio.file.*; /** * Stores general server settings like rpc host and port, global log file and * log level. - * + * * @author Stefan Meyer * @version $Id$ */ public class ServerSettings { - private static Logger logger; - private static ServerSettings instance = null; + private final Logger logger = LogManager.getLogger(this.getClass().getName()); + private static ServerSettings instance = null; - public static final long DEFAULT_MAX_FILE_SIZE = 500 * 1024 * 1024; + public static final long DEFAULT_MAX_FILE_SIZE = 500 * 1024 * 1024; - private InetAddress host; - private String hostString; - private int port; - - private String tnsAdmin = ""; + private InetAddress host; + private String hostString; + private int port; - private File indexPath; - private File logFile; - private Level logLevel; - private int numThreads = 1; - private double RAMSize = 500; - private int indexMaxFileSizeMB = 500; + private String tnsAdmin = ""; + private File indexPath; + private File logFile; + private Level logLevel; + private int numThreads = 1; + private double RAMSize = 500; + private int indexMaxFileSizeMB = 500; + private Path fopFontDirectory; - /** + + + /** + */ + private ServerSettings() { + + } + + /** + * Global singleton for all threads + * @return + * @throws ConfigurationException + */ + public static synchronized ServerSettings getInstance() throws ConfigurationException { + + if (instance == null) { + instance = new ServerSettings(); + } + return instance; + } + + /** + * Get TNS admin directory + */ + public String lookupTnsAdmin() { + + if(getTnsAdmin().length() > 0) { + return getTnsAdmin(); + } + try { + + if(System.getenv("TNS_ADMIN").length() > 0) + return System.getenv("TNS_ADMIN"); + } + catch(SecurityException e) { + logger.error("Cannot access environment variable TNS_ADMIN due to security manager limitations: " + e); + throw e; + } + return ""; + } + + public String getServerUrl() { + + String builder = "http://" + + getHostString() + + ":" + getPort() + + "/xmlrpc"; + return builder; + } + + /** + * @return the host + */ + public InetAddress getHost() { + return host; + } + + public String getHostString() { + return hostString; + } + + /** + * @param host + * The host to set. + * @throws ConfigurationException + */ + public void setHost(String host) throws ConfigurationException { + + try { + this.host = InetAddress.getByName(host); + this.hostString = host; + } + catch (UnknownHostException e) { + logger.fatal("Unknown host given: " + host); + throw new ConfigurationException(e); + } + } + + /** + * @return the port + */ + public int getPort() { + return port; + } + + /** + * @param port + * the port to set + */ + public void setPort(String port) { + this.port = Integer.parseInt(port); + } + + /** + * @return the indexPath + */ + public File getIndexPath() { + return indexPath; + } + + /** + * @return the logFile + */ + public File getLogFile() { + return logFile; + } + + /** + * @param logFile the logFile to set + * @throws ConfigurationException + * @throws IOException , ConfigurationException + */ + public void setLogFile(String logFile) throws ConfigurationException, IOException { + + this.logFile = new File(logFile); + if(!this.logFile.isAbsolute()) { + logger.error("Absolute path to logfile required: " + logFile); + throw new ConfigurationException("Absolute path to logfile required: " + logFile); + } + if(this.logFile.isDirectory()) { + logger.error("Absolute path to logfile required. Directory name given: " + logFile); + throw new ConfigurationException("Absolute path to logfile required: " + logFile); + } + if(this.logFile.createNewFile()) { + //System.out.println("Created new log file: " + this.logFile.getAbsolutePath()); + } + else { + //System.out.println("Using existing log file: " + this.logFile.getAbsolutePath()); + } + if(!this.logFile.canWrite()) { + throw new ConfigurationException("Cannot write to log file: " + logFile); + } + } + + + /** + * @return the logLevel + */ + public Level getLogLevel() { + return logLevel; + } + + /** + * @param logLevel the logLevel to set + */ + public void setLogLevel(String logLevel) { + + this.logLevel = Level.toLevel(logLevel.trim(),Level.INFO); + } + + /** + * Get tns admin directory + * @return + */ + public String getTnsAdmin() { + return tnsAdmin; + } + + /** + * Set tns admin directory + * @param tnsAdmin + */ + public void setTnsAdmin(String tnsAdmin) { + this.tnsAdmin = tnsAdmin; + } + + public void setFopFontDirectory(String directory) throws ConfigurationException + { + try { + Path path = Path.of(directory); + fopFontDirectory = Path.of(directory); + if (!Files.exists(fopFontDirectory)) { + throw new ConfigurationException("File {} does not exist"); + } + if (!Files.isReadable(path)) { + throw new ConfigurationException("File {} is not a readable"); + } + if (!Files.isDirectory(path)) { + throw new ConfigurationException("File {} is not a directory"); + } + fopFontDirectory = path; + + Files.list(fopFontDirectory).filter(p -> p.toString().endsWith(".ttf") || p.toString().endsWith(".otf")) + .forEach(p -> { + try { + Font font = Font.createFont(Font.TRUETYPE_FONT, p.toFile()); + logger.info("File: {} → family: {} name: {}", + p.getFileName(), font.getFamily(), font.getName()); + } catch (Exception e) { + logger.warn("Could not read font: {}", p, e); + } + }); + + + } catch (RuntimeException e) { + throw new ConfigurationException("Invalid path given", e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public Path getFopFontDirectory() + { + return fopFontDirectory; + } + + /** + * @param indexPath + * the indexPath to set + * @throws ConfigurationException + */ + public void setIndexPath(String indexPath) throws ConfigurationException { + + this.indexPath = new File(indexPath); + + if (!this.indexPath.isAbsolute()) { + throw new ConfigurationException("Absolute path required: " + indexPath); + } + if (!this.indexPath.canWrite()) { + throw new ConfigurationException("Path not writable: " + indexPath); + } + if (!this.indexPath.isDirectory()) { + throw new ConfigurationException("Directory name required: " + indexPath); + } + } + + + /** + * @param purgeString */ - private ServerSettings() { - - } - - /** - * Global singleton for all threads - * @return - * @throws ConfigurationException - */ - public static synchronized ServerSettings getInstance() throws ConfigurationException { - - if (instance == null) { - instance = new ServerSettings(); - } - return instance; - } - - /** - * Get TNS admin directory - */ - public String lookupTnsAdmin() { - - if(getTnsAdmin().length() > 0) { - return getTnsAdmin(); - } - try { - - if(System.getenv("TNS_ADMIN").length() > 0) - return System.getenv("TNS_ADMIN"); - } - catch(SecurityException e) { - logger.error("Cannot access environment variable TNS_ADMIN due to security manager limitations: " + e); - throw e; - } - return ""; - } - - public String getServerUrl() { - - String builder = "http://" + - getHostString() + - ":" + getPort() + - "/xmlrpc"; - return builder; - } - - /** - * @return the host - */ - public InetAddress getHost() { - return host; - } - - public String getHostString() { - return hostString; - } - - /** - * @param host - * The host to set. - * @throws ConfigurationException - */ - public void setHost(String host) throws ConfigurationException { - - try { - this.host = InetAddress.getByName(host); - this.hostString = host; - } - catch (UnknownHostException e) { - logger.fatal("Unknown host given: " + host); - throw new ConfigurationException(e); - } - } - - /** - * @return the port - */ - public int getPort() { - return port; - } - - /** - * @param port - * the port to set - */ - public void setPort(String port) { - this.port = Integer.parseInt(port); - } - - /** - * @return the indexPath - */ - public File getIndexPath() { - return indexPath; - } - - /** - * @return the logFile - */ - public File getLogFile() { - return logFile; - } - - /** - * @param logFile the logFile to set - * @throws ConfigurationException - * @throws IOException , ConfigurationException - */ - public void setLogFile(String logFile) throws ConfigurationException, IOException { - - this.logFile = new File(logFile); - if(!this.logFile.isAbsolute()) { - logger.error("Absolute path to logfile required: " + logFile); - throw new ConfigurationException("Absolute path to logfile required: " + logFile); - } - if(this.logFile.isDirectory()) { - logger.error("Absolute path to logfile required. Directory name given: " + logFile); - throw new ConfigurationException("Absolute path to logfile required: " + logFile); - } - if(this.logFile.createNewFile()) { - //System.out.println("Created new log file: " + this.logFile.getAbsolutePath()); - } - else { - //System.out.println("Using existing log file: " + this.logFile.getAbsolutePath()); - } - if(!this.logFile.canWrite()) { - throw new ConfigurationException("Cannot write to log file: " + logFile); - } - } - - - /** - * @return the logLevel - */ - public Level getLogLevel() { - return logLevel; - } - - /** - * @param logLevel the logLevel to set - */ - public void setLogLevel(String logLevel) { - - this.logLevel = Level.toLevel(logLevel.trim(),Level.INFO); - } - - /** - * Get tns admin directory - * @return - */ - public String getTnsAdmin() { - return tnsAdmin; - } - - /** - * Set tns admin directory - * @param tnsAdmin - */ - public void setTnsAdmin(String tnsAdmin) { - this.tnsAdmin = tnsAdmin; - } - - - /** - * @param indexPath - * the indexPath to set - * @throws ConfigurationException - */ - public void setIndexPath(String indexPath) throws ConfigurationException { - - this.indexPath = new File(indexPath); - - if (!this.indexPath.isAbsolute()) { - throw new ConfigurationException("Absolute path required: " + indexPath); - } - if (!this.indexPath.canWrite()) { - throw new ConfigurationException("Path not writable: " + indexPath); - } - if (!this.indexPath.isDirectory()) { - throw new ConfigurationException("Directory name required: " + indexPath); - } - } - - - /** - * @param purgeString - */ - public void setThreadNumber(String purgeString) { - - this.numThreads = Integer.valueOf(purgeString); - } - - public int getNumThreads() { - return numThreads; - } - - public double getRAMSize() { - return RAMSize; - } - - public void setRAMSize(String purgedString) { - - RAMSize = Double.valueOf(purgedString); - } - - public int getMaxFileSizeMB() - { - return indexMaxFileSizeMB; - } - - public long getMaxFileSize() - { - return (long) indexMaxFileSizeMB * 1024 * 1024; - } - - public void setMaxFileSizeMB(String mb) - { - this.indexMaxFileSizeMB = Integer.valueOf(mb); - } + public void setThreadNumber(String purgeString) { + + this.numThreads = Integer.valueOf(purgeString); + } + + public int getNumThreads() { + return numThreads; + } + + public double getRAMSize() { + return RAMSize; + } + + public void setRAMSize(String purgedString) { + + RAMSize = Double.valueOf(purgedString); + } + + public int getMaxFileSizeMB() + { + return indexMaxFileSizeMB; + } + + public long getMaxFileSize() + { + return (long) indexMaxFileSizeMB * 1024 * 1024; + } + + public void setMaxFileSizeMB(String mb) + { + this.indexMaxFileSizeMB = Integer.valueOf(mb); + } } diff --git a/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/transformation/FO2PDF.java b/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/transformation/FO2PDF.java index 59354c6ed94f..32341e2af813 100755 --- a/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/transformation/FO2PDF.java +++ b/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/transformation/FO2PDF.java @@ -23,22 +23,21 @@ package de.ilias.services.transformation; +import de.ilias.services.settings.ConfigurationException; +import de.ilias.services.settings.ServerSettings; import org.apache.fop.apps.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.xml.sax.SAXException; import javax.xml.transform.*; import javax.xml.transform.sax.SAXResult; import javax.xml.transform.stream.StreamSource; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.net.URISyntaxException; +import java.io.*; import java.nio.charset.StandardCharsets; import java.util.List; -public class FO2PDF { +public class FO2PDF +{ private static FO2PDF instance = null; @@ -50,20 +49,22 @@ public class FO2PDF { /** * Singleton constructor */ - public FO2PDF() { + public FO2PDF() throws TransformationException + { try { - fopFactory = FopFactory.newInstance(getClass().getResource("/de/ilias/config/fopConfig.xml").toURI()); - } catch (URISyntaxException | NullPointerException ex) { - logger.error("Cannot load fop configuration:" + ex); + FOConfigBuilder configBuilder = new FOConfigBuilder(ServerSettings.getInstance()); + fopFactory = configBuilder.buildFopFactory(); + } catch (IOException | ConfigurationException e) { + logger.error("FOP configuration error", e); + throw new TransformationException(e); } - } /** * Get FO2PDF instance */ - public static FO2PDF getInstance() { - + public static FO2PDF getInstance() throws TransformationException + { if (instance == null) { return instance = new FO2PDF(); } @@ -73,20 +74,20 @@ public static FO2PDF getInstance() { /** * clear fop uri cache */ - public void clearCache() { - + public void clearCache() + { fopFactory.getImageManager().getCache().clearCache(); } public void transform() - throws TransformationException { + throws TransformationException + { try { logger.info("Starting fop transformation..."); FOUserAgent foUserAgent = fopFactory.newFOUserAgent(); -// foUserAgent.setTargetResolution(300); ByteArrayOutputStream out = new ByteArrayOutputStream(); Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, out); @@ -128,7 +129,8 @@ public void transform() /** * @return Returns the foString. */ - public String getFoString() { + public String getFoString() + { return foString; } @@ -136,20 +138,24 @@ public String getFoString() { /** * @param foString The foString to set. */ - public void setFoString(String foString) { + public void setFoString(String foString) + { this.foString = foString; } - public byte[] getPdf() { + public byte[] getPdf() + { return this.pdfByteArray; } - public void setPdf(byte[] ba) { + public void setPdf(byte[] ba) + { this.pdfByteArray = ba; } - private InputStream getFoInputStream() { + private InputStream getFoInputStream() + { return new ByteArrayInputStream(getFoString().getBytes(StandardCharsets.UTF_8)); } } diff --git a/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/transformation/FOConfigBuilder.java b/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/transformation/FOConfigBuilder.java new file mode 100644 index 000000000000..19725ae5225e --- /dev/null +++ b/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/transformation/FOConfigBuilder.java @@ -0,0 +1,122 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +package de.ilias.services.transformation; + +import de.ilias.services.settings.ServerSettings; +import org.apache.fop.apps.FopConfParser; +import org.apache.fop.apps.FopFactory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.xml.sax.SAXException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class FOConfigBuilder +{ + private final Logger logger = LogManager.getLogger(this.getClass().getName()); + + private ServerSettings settings; + + private static String fopConfigTemplate = """ + + + true + false + + + + %s + + + + + """.stripLeading(); + private List fontDirectories = new ArrayList<>(); + private Path tempDirectory = null; + + public FOConfigBuilder(ServerSettings settings) { + this.settings = settings; + } + + public FopFactory buildFopFactory() throws IOException, TransformationException + { + String directoryEntries = ""; + String fopXmlConfig = ""; + + initDirectories(); + directoryEntries = fontDirectories.stream() + .map(p -> "%s".formatted(p.toString())) + .collect(Collectors.joining("\n")); + fopXmlConfig = fopConfigTemplate.formatted(directoryEntries); + logger.info("Using config {}", fopXmlConfig); + + InputStream in = new ByteArrayInputStream( + fopXmlConfig.getBytes(StandardCharsets.UTF_8) + ); + try { + return new FopConfParser( + in, + tempDirectory.toUri() + ) + .getFopFactoryBuilder().build(); + } catch (SAXException e) { + logger.error("Cannot configure FOP environment", e); + throw new TransformationException(e); + } + } + + private void initDirectories() throws TransformationException + { + try { + initUnifont(); + initCustomDirectory(); + } catch (IOException e) { + throw new TransformationException(e); + } + } + + private void initUnifont() throws IOException { + tempDirectory = Files.createTempDirectory("ilias-fonts-"); + Path tempFont = tempDirectory.resolve("unifont.ttf"); + tempDirectory.toFile().deleteOnExit(); + InputStream in = FO2PDF.class.getResourceAsStream("/de/ilias/config/fonts/unifont.ttf"); + Files.copy(in, tempFont, StandardCopyOption.REPLACE_EXISTING); + + fontDirectories.add(tempDirectory); + } + + private void initCustomDirectory() + { + if (settings.getFopFontDirectory() == null) { + logger.info("No custom fonts configured"); + return; + } + logger.info("Using font directory {}", settings.getFopFontDirectory()); + fontDirectories.add(settings.getFopFontDirectory()); + } +} diff --git a/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/transformation/RPCTransformationHandler.java b/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/transformation/RPCTransformationHandler.java index 8ffc7ec1d300..f29308cba9cc 100755 --- a/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/transformation/RPCTransformationHandler.java +++ b/components/ILIAS/WebServices/RPC/lib/src/main/java/de/ilias/services/transformation/RPCTransformationHandler.java @@ -22,42 +22,36 @@ package de.ilias.services.transformation; - - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class RPCTransformationHandler { protected static Logger logger = LogManager.getLogger(RPCTransformationHandler.class); - - public RPCTransformationHandler() { - + public RPCTransformationHandler() { } - + public boolean ping() { - + return true; } - - public byte[] ilFO2PDF(String foString) { - - FO2PDF fo = null; - - try { - - fo = new FO2PDF(); - fo.clearCache(); - fo.setFoString(foString); - fo.transform(); - - return fo.getPdf(); - } - catch (TransformationException e) { - - logger.warn("Transformation failed:" + e); - } + + public byte[] ilFO2PDF(String foString) { + + FO2PDF fo = null; + + try { + fo = FO2PDF.getInstance(); + fo.clearCache(); + fo.setFoString(foString); + fo.transform(); + + return fo.getPdf(); + } + catch (TransformationException e) { + logger.warn("Transformation failed:" + e); + } return null; } } diff --git a/components/ILIAS/WebServices/RPC/lib/src/main/resources/de/ilias/config/fonts/unifont.ttf b/components/ILIAS/WebServices/RPC/lib/src/main/resources/de/ilias/config/fonts/unifont.ttf new file mode 100644 index 000000000000..764ee1f7e078 Binary files /dev/null and b/components/ILIAS/WebServices/RPC/lib/src/main/resources/de/ilias/config/fonts/unifont.ttf differ