Skip to content

Commit a3c5955

Browse files
committed
Updated the project to v1.2 with the following:
- Added support for Custom Font integration as Resource Files in the project and deployed with the JAR! - Added a a couple sample (free) custom fonts and sample markup `resources/samples/WorkinWithFontsSampe.fo` in the project. - Fixed bug in the render Event Log debug details returned in the Http Header whereby Apache FOP may send Unicode Characters but only ASCII are allowed; Unicode are now safely escaped into their respective Hex Codes so that the message is readable. - Fixed issue in Maven build to enforce the clean stage so the artifact always contains the latest changes (e.g. especially physical resource file changes) when debugging. - Some miscellaneous code cleanup.
1 parent f00da67 commit a3c5955

File tree

16 files changed

+393
-19
lines changed

16 files changed

+393
-19
lines changed

README.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@ then I do love-me-some-coffee!*
1414
</a>
1515

1616
## Updates
17-
Updated the project to v1.1 as it now incorporates the following new updates:
17+
**Updated the project to v1.2 with the following:**
18+
- Added support for Custom Font integration as Resource Files in the project and deployed with the JAR!
19+
- This enables adding fonts easily by simply dropping them in the `resources/fonts` folder, and then registering them via configuration in the `apache-fop-config.xml` according to [Apache FOP Font Documentation](https://xmlgraphics.apache.org/fop/2.6/fonts.html#register).
20+
- Added a a couple sample (free) custom fonts and sample markup `resources/samples/WorkinWithFontsSampe.fo` in the project.
21+
- Fixed bug in the render Event Log debug details returned in the Http Header whereby Apache FOP may send Unicode Characters but only ASCII are allowed; Unicode are now safely escaped into their respective Hex Codes so that the message is readable.
22+
- Fixed issue in Maven build to enforce the clean stage so the artifact always contains the latest changes (e.g. especially physical resource file changes) when debugging.
23+
- Some miscellaneous code cleanup.
24+
25+
Updated the project to v1.1 as it now incorporates:
1826
- Upgraded the project to use Java v11 now as the latest long term supported (LTS) version for Azure Functions (aka Zulu Java v11)
1927
- _Previously was Java 8 (v1.8) (aka Zulu Java v8)._
2028
- Bumping the versions of all dependencies to the latest stable versions
@@ -97,7 +105,7 @@ Here's the high level steps to get started...
97105

98106
## Additional Features:
99107

100-
#### GZIP Compression:
108+
### GZIP Compression:
101109
In addition to rendering the PDF, this service already implements GZIP compression for returning the Binary PDF files. This can greatly improve performance and download times, especially for PDFs rendered without much imagery, as the text content bytes tends to be more compressible.
102110

103111
All you need to do is add the `Accept-Encoding` header with a value of `gzip` to the request:
@@ -108,6 +116,45 @@ Accept-Encoding=gzip
108116
<img src="/postman-header-enable-gzip.png" style="width:auto;height:auto;max-width:800px;">
109117
</p>
110118

119+
### Custom Fonts via Resource Files:
120+
##### Add Font Files (*.ttf, *.otf, etc.)
121+
To easily utilize custom fonts with the Azure Functions deployment, this project provides the ability to simply add them into the project as resource
122+
files by simply placing them in the `src/main/resources/fonts` folder. So you can literally just copy them into the project and deploy. In IntelliJ IDEA the structure will look like:
123+
<p align="center">
124+
<img src="/intellij-resources-fonts-project-structure.png" style="width:auto;height:auto;max-width:500px;">
125+
</p>
126+
127+
Once there they can be resolved at runtime by the application even after being deployed to Azure Functions; because they will be embedded resources with the JAR file. ApacheFOP.Serverless has a custom `ResourceResolver` implementation that can then locate these via relative path is used when registering the fonts via configuration in the `apache-fop-config.xml` according to [Apache FOP Font Documentation](https://xmlgraphics.apache.org/fop/2.6/fonts.html#register).
128+
129+
##### Register them with Apache FOP Configuration:
130+
Now we need to register the fonts we've placed into our resources. We do this by editing the `resources/apache-fop-config.xml` according to [Apache FOP Font Documentation](https://xmlgraphics.apache.org/fop/2.6/fonts.html#register). The project includes a couple sample fonts as well as a sample Xsl FO markup file that will render using these new custom fonts located at `resources/samples/WorkinWithFontsSample.fo`.
131+
132+
The font registration configuration looks like this:
133+
```
134+
<fop version="1.0">
135+
. . .
136+
<renderers>
137+
<renderer mime="application/pdf">
138+
. . .
139+
140+
<fonts>
141+
<!-- Automatically detect operating system installed fonts; More Details Here: https://xmlgraphics.apache.org/fop/2.6/fonts.html -->
142+
<auto-detect/>
143+
144+
<font embed-url="fonts/Minisystem.ttf" simulate-style="true">
145+
<font-triplet name="Minisystem" style="normal" weight="normal" />
146+
</font>
147+
<font embed-url="fonts/PixelWow.otf" simulate-style="true">
148+
<font-triplet name="PixelWow" style="normal" weight="normal" />
149+
</font>
150+
</fonts>
151+
. . .
152+
</renderer>
153+
</renderers>
154+
</fop>
155+
```
156+
157+
111158
## Calling the Service from .Net
112159

113160
### Snippet:

apachefop-serverless-az-func/.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ hs_err_pid*
2828
.settings/
2929
.project
3030
.classpath
31-
.vscode/
3231

3332
# macOS
3433
.DS_Store

apachefop-serverless-az-func/pom.xml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<groupId>com.cajuncoding</groupId>
66
<artifactId>apachefop-serverless-az-func</artifactId>
7-
<version>1.1-SNAPSHOT</version>
7+
<version>1.2-SNAPSHOT</version>
88
<packaging>jar</packaging>
99

1010
<name>Azure Java Functions</name>
@@ -14,6 +14,8 @@
1414
<java.version>11</java.version>
1515
<appBaseName>apachefop-serverless-az-func</appBaseName>
1616
<stagingDirectory>${project.build.directory}/azure-functions/${appBaseName}</stagingDirectory>
17+
<functionAppName>apachefop-serverless-az-func</functionAppName>
18+
<!-- <functionAppRegion>westus</functionAppRegion> -->
1719
</properties>
1820

1921
<dependencies>
@@ -32,6 +34,12 @@
3234
<artifactId>commons-lang3</artifactId>
3335
<version>3.12.0</version>
3436
</dependency>
37+
<dependency>
38+
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-text -->
39+
<groupId>org.apache.commons</groupId>
40+
<artifactId>commons-text</artifactId>
41+
<version>1.9</version>
42+
</dependency>
3543
<dependency>
3644
<groupId>commons-io</groupId>
3745
<artifactId>commons-io</artifactId>
@@ -154,6 +162,7 @@
154162
<artifactId>maven-resources-plugin</artifactId>
155163
<version>3.2.0</version>
156164
<executions>
165+
<!-- COPY Azure Function configuration JSON Files (required especially for running locally)! -->
157166
<execution>
158167
<id>copy-resources</id>
159168
<phase>package</phase>
@@ -210,6 +219,15 @@
210219
</fileset>
211220
</filesets>
212221
</configuration>
222+
<executions>
223+
<execution>
224+
<id>auto-clean</id>
225+
<phase>initialize</phase>
226+
<goals>
227+
<goal>clean</goal>
228+
</goals>
229+
</execution>
230+
</executions>
213231
</plugin>
214232
</plugins>
215233
</build>

apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/ApacheFopFunction.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
package com.cajuncoding.apachefop.serverless;
22

3+
import com.cajuncoding.apachefop.serverless.apachefop.ApacheFopRenderer;
34
import com.cajuncoding.apachefop.serverless.web.ApacheFopServerlessFunctionExecutor;
45
import com.microsoft.azure.functions.*;
56
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
67
import com.microsoft.azure.functions.annotation.FunctionName;
78
import com.microsoft.azure.functions.annotation.HttpTrigger;
89

910
import java.util.Optional;
11+
import java.util.logging.Level;
1012

1113
/**
1214
* Azure Functions with HTTP Trigger.
1315
*/
1416
public class ApacheFopFunction {
1517
/**
16-
* This function listens at endpoint "/api/apache-fop/xslfo". Two ways to invoke it using "curl" command in bash:
18+
* This function listens at endpoint "/api/apache-fop/xslfo".
1719
*/
1820
@FunctionName("ApacheFOP")
1921
public HttpResponseMessage run(

apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/ApacheFopGzipFunction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*/
1414
public class ApacheFopGzipFunction {
1515
/**
16-
* This function listens at endpoint "/api/apache-fop/xslfo". Two ways to invoke it using "curl" command in bash:
16+
* This function listens at endpoint "/api/apache-fop/gzip".
1717
*/
1818
@FunctionName("ApacheFOPGzip")
1919
public HttpResponseMessage run(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.cajuncoding.apachefop.serverless.apachefop;
2+
3+
import com.cajuncoding.apachefop.serverless.utils.ResourceUtils;
4+
import org.apache.fop.apps.io.ResourceResolverFactory;
5+
import org.apache.xmlgraphics.io.Resource;
6+
import org.apache.xmlgraphics.io.ResourceResolver;
7+
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.io.OutputStream;
11+
import java.net.URI;
12+
import java.nio.file.Paths;
13+
14+
public class ApacheFopJavaResourcesFileResolver implements ResourceResolver {
15+
16+
public static final String FILE_SCHEME = "file";
17+
18+
private static final ResourceResolver defaultFopResolver = ResourceResolverFactory.createDefaultResourceResolver();
19+
20+
@Override
21+
public Resource getResource(URI uri) throws IOException {
22+
InputStream resourceStream = null;
23+
24+
//We MUST ONLY attempt to handle FILE requests... any Http/Https requests need to use original/default behaviour!
25+
if(uri.getScheme().equalsIgnoreCase(FILE_SCHEME)) {
26+
//Map the requested Uri to the base application path to determine it's relative path as a Resource!
27+
var requestPath = Paths.get(uri);
28+
var mappedPath = ResourceUtils.MapServerPath(requestPath);
29+
30+
//Now with the relative path for our resource we can attempt to retrieve it...
31+
resourceStream = ResourceUtils.LoadResourceAsStream(mappedPath.toString());
32+
}
33+
34+
//If the resource was located then we return it...
35+
//Otherwise we fallback to default Apache FOP behaviour using the original default Fop Resource Resolver...
36+
return resourceStream != null
37+
? new Resource(resourceStream)
38+
: defaultFopResolver.getResource(uri);
39+
}
40+
41+
@Override
42+
public OutputStream getOutputStream(URI uri) throws IOException {
43+
//For Output Streams we simply default to original Apache FOP behaviour using the original default Fop Resource Resolver...
44+
return defaultFopResolver.getOutputStream(uri);
45+
46+
//return ResourceUtils.GetClassLoader()
47+
// .getResource(uri.toString())
48+
// .openConnection()
49+
// .getOutputStream();
50+
}
51+
}

apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/apachefop/ApacheFopRenderer.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.cajuncoding.apachefop.serverless.apachefop;
22

3+
import com.cajuncoding.apachefop.serverless.config.ApacheFopServerlessConfig;
34
import com.cajuncoding.apachefop.serverless.config.ApacheFopServerlessConstants;
45
import org.apache.fop.apps.*;
56
import org.apache.fop.configuration.ConfigurationException;
@@ -14,6 +15,9 @@
1415
import java.util.zip.GZIPOutputStream;
1516

1617
public class ApacheFopRenderer {
18+
//Provide Dynamic resolution of Resources (e.g. Fonts) from JAR Resource Files...
19+
private static final ApacheFopJavaResourcesFileResolver fopResourcesFileResolver = new ApacheFopJavaResourcesFileResolver();
20+
1721
//FOPFactory is expected to be re-used as noted in Apache 'overview' section here:
1822
// https://xmlgraphics.apache.org/fop/1.1/embedding.html
1923
private static final FopFactory fopFactory = createApacheFopFactory();
@@ -22,9 +26,10 @@ public class ApacheFopRenderer {
2226
private static final TransformerFactory transformerFactory = TransformerFactory.newInstance();
2327

2428
private Logger logger = null;
29+
private ApacheFopServerlessConfig apacheFopConfig = null;
2530

2631
public ApacheFopRenderer() {
27-
this.logger = null;
32+
this(null);
2833
}
2934

3035
public ApacheFopRenderer(Logger optionalLogger) {
@@ -85,7 +90,8 @@ public static FopFactory createApacheFopFactory() {
8590
//Attempt to initialize with Configuration loaded from Configuration XML Resource file...
8691
var cfgBuilder = new DefaultConfigurationBuilder();
8792
var cfg = cfgBuilder.build(configStream);
88-
var fopFactoryBuilder = new FopFactoryBuilder(baseUri).setConfiguration(cfg);
93+
94+
var fopFactoryBuilder = new FopFactoryBuilder(baseUri, fopResourcesFileResolver).setConfiguration(cfg);
8995

9096
fopFactory = fopFactoryBuilder.build();
9197
}

apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/utils/ResourceUtils.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,50 @@
55
import org.apache.commons.io.IOUtils;
66

77
import java.io.IOException;
8+
import java.io.InputStream;
89
import java.nio.charset.StandardCharsets;
10+
import java.nio.file.Path;
11+
import java.nio.file.Paths;
912

1013
public class ResourceUtils {
1114

1215
public static String LoadResourceAsString(String resource) throws IOException {
13-
return LoadResourceAsString(resource, ResourceUtils.class.getClassLoader());
16+
return LoadResourceAsString(resource, GetClassLoader());
1417
}
1518

1619
public static String LoadResourceAsString(String resource, ClassLoader resourceClassLoader) throws IOException {
17-
try (var xslFoStream = resourceClassLoader.getResourceAsStream(resource);) {
18-
var resourceText = IOUtils.toString(xslFoStream, StandardCharsets.UTF_8);
20+
var sanitizedResource = SanitizeResourcePath(resource);
21+
22+
try (var resourceStream = resourceClassLoader.getResourceAsStream(sanitizedResource);) {
23+
var resourceText = IOUtils.toString(resourceStream, StandardCharsets.UTF_8);
1924
return resourceText;
2025
}
2126
}
27+
28+
public static InputStream LoadResourceAsStream(String resource) {
29+
var sanitizedResource = SanitizeResourcePath(resource);
30+
var resourceStream = GetClassLoader().getResourceAsStream(sanitizedResource);
31+
return resourceStream;
32+
}
33+
34+
public static Path GetBaseMappedPath() {
35+
var baseAppPath = Paths.get("").toAbsolutePath();
36+
return baseAppPath;
37+
}
38+
39+
public static Path MapServerPath(Path pathToMap) {
40+
var basePath = GetBaseMappedPath();
41+
var relativePath = basePath.relativize(pathToMap);
42+
return relativePath;
43+
}
44+
45+
public static ClassLoader GetClassLoader() {
46+
return ResourceUtils.class.getClassLoader();
47+
}
48+
49+
public static String SanitizeResourcePath(String resource) {
50+
var trimmed = resource.trim();
51+
var slashCorrected = trimmed.indexOf('\\') < 0 ? trimmed : trimmed.replace('\\', '/');
52+
return slashCorrected;
53+
}
2254
}

apachefop-serverless-az-func/src/main/java/com/cajuncoding/apachefop/serverless/utils/TextUtils.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ public static String getCurrentW3cDateTime() {
1818
return currentW3cDateTime;
1919
}
2020

21+
public static boolean IsNullOrWhiteSpace(String value) {
22+
return value == null || value.isBlank();
23+
}
24+
2125
/**
2226
* Truncates a string to the number of characters that fit in X bytes avoiding multi byte characters being cut in
2327
* half at the cut off point. Also handles surrogate pairs where 2 characters in the string is actually one literal
Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
11
package com.cajuncoding.apachefop.serverless.web;
22

3+
import com.cajuncoding.apachefop.serverless.http.HttpEncodings;
4+
import com.cajuncoding.apachefop.serverless.utils.TextUtils;
5+
import org.apache.commons.text.StringEscapeUtils;
6+
import org.w3c.dom.Text;
7+
8+
import java.io.UnsupportedEncodingException;
9+
310
public class SafeHeader {
411
private String value;
512
private String encoding;
6-
public SafeHeader(String value, String encoding) {
7-
this.value = value;
8-
this.encoding = encoding;
13+
14+
public SafeHeader(String value, String encoding) throws UnsupportedEncodingException {
15+
this.value = sanitizeTextForHttpHeader(value, encoding);
16+
this.encoding = sanitizeTextForHttpHeader(encoding, null);
917
}
1018

1119
public String getEncoding() { return encoding; }
1220
public String getValue() { return value; }
21+
22+
protected String sanitizeTextForHttpHeader(String value, String encoding) throws UnsupportedEncodingException {
23+
if(TextUtils.IsNullOrWhiteSpace(value))
24+
return value;
25+
26+
if(encoding != null && encoding != HttpEncodings.IDENTITY_ENCODING)
27+
return value;
28+
29+
//BBernard - 09/29/2021
30+
//FIX bug where ApacheFOP may return Unicode Characters in Event Messages whereby we must escape any
31+
// Unicode Characters in the Header Text because onlY ASCII characters are valid.
32+
var sanitizedValue = StringEscapeUtils.escapeJava(value);
33+
return sanitizedValue;
34+
}
1335
}

0 commit comments

Comments
 (0)