Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 3.0.1.3

- [LDEV-6243](https://luceeserver.atlassian.net/browse/LDEV-6243) fix member function race condition under concurrent requests
- [LDEV-6244](https://luceeserver.atlassian.net/browse/LDEV-6244) replace GetApplicationSettings BIF with getCustom() to avoid DummyWSHandler exception spam
- [LDEV-6245](https://luceeserver.atlassian.net/browse/LDEV-6245) performance: remove redundant coder cache, fix double write, reduce Tika MIME detection calls, eliminate exception-driven format matching

## 3.0.1.2 (2026-03-26)

- [LDEV-5129](https://luceeserver.atlassian.net/browse/LDEV-5129) remove unused bundled jars: commons-io (CVE-2024-47554), xmpcore, apiguardian, hamcrest, opentest4j
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.lucee</groupId>
<artifactId>image-extension</artifactId>
<version>3.0.1.2-SNAPSHOT</version>
<version>3.0.1.3-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Image Extension</name>

Expand Down
46 changes: 27 additions & 19 deletions source/java/src/org/lucee/extension/image/ImageUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,14 @@

public class ImageUtil {

private static Coder _coder;

private static final boolean useSunCodec = getSunCodec();
private static Class JPEGCodec;
private static Class JPEGEncodeParam;

private static int counter = 0;

private static Coder getCoder() {
if (true || _coder == null) {
_coder = Coder.getInstance(CFMLEngineFactory.getInstance().getThreadPageContext());
}
return _coder;
return Coder.getInstance(CFMLEngineFactory.getInstance().getThreadPageContext());
}

public static String getOneWriterFormatName(String... preferences) throws IOException {
Expand Down Expand Up @@ -179,23 +174,34 @@ public static byte[] readBase64(String b64str, StringBuilder mimetype) throws IO
return Base64.decodeBase64(b64str.getBytes());
}

/**
* Detect the image format for a resource. The detection order is:
* 1. Coder-specific magic byte detection (JDeli, TwelveMonkeys etc) — handles misnamed files
* 2. MIME type detection via Tika (content-based) — fallback for formats coders don't recognise
* 3. File extension — last resort, trusts the filename
*
* MIME type (Tika) is resolved once and passed to all coders to avoid repeated detection.
* This is critical for performance — Tika's magic byte scanning is expensive (~600k allocations
* per 5k image ops when called per-coder).
*
* Misnamed files (e.g. a JPEG saved as .png) are handled by steps 1 and 2 which inspect
* actual file content, not the extension.
*/
public static String getFormat(Resource res) throws IOException {
long len = res.length();
// resolve MIME type once and pass to coders — avoids repeated Tika detection
String mt = len > 0 ? getMimeType(res, null) : null;
Coder c = getCoder();
if (c instanceof FormatExtract) {
String format = ((FormatExtract) c).getFormat(res, null);
String format = ((FormatExtract) c).getFormat(res, mt, null);
if (!Util.isEmpty(format, true)) {
return format;
}
}
// there is no need to check the mime type if the file is empty
if (len > 0) {
String mt = getMimeType(res, null);
if (!Util.isEmpty(mt)) {
String format = getImageFormatFromMimeType(mt, null);
if (!Util.isEmpty(format)) {
return format;
}
if (!Util.isEmpty(mt)) {
String format = getImageFormatFromMimeType(mt, null);
if (!Util.isEmpty(format)) {
return format;
}
}
return getFormatFromExtension(res, null);
Expand All @@ -210,21 +216,23 @@ public static String getMimeType(byte[] binary, String defaultValue) {
}

public static String getFormat(byte[] binary) throws IOException {
String mt = CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(binary, "");
Coder c = getCoder();
if (c instanceof FormatExtract) {
String format = ((FormatExtract) c).getFormat(binary, null);
String format = ((FormatExtract) c).getFormat(binary, mt, null);
if (!Util.isEmpty(format, true)) return format;
}
return getFormatFromMimeType(CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(binary, ""));
return getFormatFromMimeType(mt);
}

public static String getFormat(byte[] binary, String defaultValue) {
String mt = CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(binary, "");
Coder c = getCoder();
if (c instanceof FormatExtract) {
String format = ((FormatExtract) c).getFormat(binary, null);
String format = ((FormatExtract) c).getFormat(binary, mt, null);
if (!Util.isEmpty(format, true)) return format;
}
return getImageFormatFromMimeType(CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(binary, ""), defaultValue);
return getImageFormatFromMimeType(mt, defaultValue);
}

public static String toFormat(String format) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ public BufferedImage read(byte[] bytes, String format) throws IOException {

@Override
public void write(Image img, Resource destination, String format, float quality, boolean noMeta) throws IOException {
if (destination instanceof File) writeImage(img, destination, format, quality, noMeta);
if (destination instanceof File) {
writeImage(img, destination, format, quality, noMeta);
return;
}
OutputStream os = null;
try {
os = destination.getOutputStream();
Expand Down Expand Up @@ -169,6 +172,14 @@ public String getFormat(Resource res, String defaultValue) {

}

@Override
public String getFormat(Resource res, String mimeType, String defaultValue) {
if (!Util.isEmpty(mimeType)) {
return getFormatbyMimeType(mimeType, defaultValue);
}
return getFormat(res, defaultValue);
}

@Override
public String getFormat(byte[] bytes) throws IOException {
return getFormatbyMimeType(CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(bytes, null));
Expand All @@ -179,30 +190,30 @@ public String getFormat(byte[] bytes, String defaultValue) {
return getFormatbyMimeType(CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(bytes, null), defaultValue);
}

private String getFormatbyMimeType(String mimeType, String defaultValue) {
@Override
public String getFormat(byte[] bytes, String mimeType, String defaultValue) {
if (!Util.isEmpty(mimeType)) {
try {
return getFormatbyMimeType(mimeType);
}
catch (Throwable t) {
if (t instanceof ThreadDeath) throw (ThreadDeath) t;
}
return getFormatbyMimeType(mimeType, defaultValue);
}
return defaultValue;
return getFormat(bytes, defaultValue);
}

private String getFormatbyMimeType(String mimeType) throws IOException {
private String getFormatbyMimeType(String mimeType, String defaultValue) {
if (!Util.isEmpty(mimeType)) {

for (Map.Entry<String, Codec> e: codecs.entrySet()) {
for (String mt: e.getValue().mimeTypes) {

if (mimeType.equalsIgnoreCase(mt)) {
return e.getKey();
}
}
}
}
return defaultValue;
}

private String getFormatbyMimeType(String mimeType) throws IOException {
String result = getFormatbyMimeType(mimeType, null);
if (result != null) return result;
throw new IOException("no matching format found for mimetype [" + mimeType + "]");
}

Expand Down
34 changes: 23 additions & 11 deletions source/java/src/org/lucee/extension/image/coder/ImageIOCoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ public String getFormat(Resource res, String defaultValue) {
return defaultValue;
}

@Override
public String getFormat(Resource res, String mimeType, String defaultValue) {
if (!Util.isEmpty(mimeType)) {
return getFormatbyMimeType(mimeType, defaultValue);
}
return getFormat(res, defaultValue);
}

@Override
public String getFormat(byte[] bytes) throws IOException {
return getFormatbyMimeType(CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(bytes, null));
Expand All @@ -149,27 +157,31 @@ public String getFormat(byte[] bytes, String defaultValue) {
return getFormatbyMimeType(CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(bytes, null), defaultValue);
}

@Override
public String getFormat(byte[] bytes, String mimeType, String defaultValue) {
if (!Util.isEmpty(mimeType)) {
return getFormatbyMimeType(mimeType, defaultValue);
}
return getFormat(bytes, defaultValue);
}

private String getFormatbyMimeType(String mimeType, String defaultValue) {
if (!Util.isEmpty(mimeType)) {
try {
return getFormatbyMimeType(mimeType);
Iterator<ImageReader> it = ImageIO.getImageReadersByMIMEType(mimeType);
while (it != null && it.hasNext()) {
return it.next().getFormatName();
}
}
catch (Throwable t) {
if (t instanceof ThreadDeath) throw (ThreadDeath) t;
catch (IOException e) {
}
}
return defaultValue;
}

private String getFormatbyMimeType(String mimeType) throws IOException {
if (!Util.isEmpty(mimeType)) {
Iterator<ImageReader> it = ImageIO.getImageReadersByMIMEType(mimeType);
while (it != null && it.hasNext()) {
String fn = it.next().getFormatName();
return fn;
}

}
String result = getFormatbyMimeType(mimeType, null);
if (result != null) return result;
throw new IOException("no matching format found for mimetype [" + mimeType + "]");
}

Expand Down
14 changes: 12 additions & 2 deletions source/java/src/org/lucee/extension/image/coder/MultiCoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,14 @@ public String getFormat(byte[] bytes) throws IOException {

@Override
public String getFormat(Resource res, String defaultValue) {
return getFormat(res, null, defaultValue);
}

@Override
public String getFormat(Resource res, String mimeType, String defaultValue) {
for (Coder coder: coders) {
if (!(coder instanceof FormatExtract)) continue;
String format = ((FormatExtract) coder).getFormat(res, null);
String format = ((FormatExtract) coder).getFormat(res, mimeType, null);
if (!Util.isEmpty(format)) {
return format;
}
Expand All @@ -288,9 +293,14 @@ public String getFormat(Resource res, String defaultValue) {

@Override
public String getFormat(byte[] bytes, String defaultValue) {
return getFormat(bytes, null, defaultValue);
}

@Override
public String getFormat(byte[] bytes, String mimeType, String defaultValue) {
for (Coder coder: coders) {
if (!(coder instanceof FormatExtract)) continue;
String format = ((FormatExtract) coder).getFormat(bytes, null);
String format = ((FormatExtract) coder).getFormat(bytes, mimeType, null);
if (!Util.isEmpty(format)) return format;
}
return defaultValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,13 @@ public interface FormatExtract {
public abstract String getFormat(byte[] bytes) throws IOException;

public abstract String getFormat(byte[] bytes, String defaultValue);

// overloads that accept a pre-resolved MIME type to avoid repeated Tika detection
default String getFormat(Resource res, String mimeType, String defaultValue) {
return getFormat(res, defaultValue);
}

default String getFormat(byte[] bytes, String mimeType, String defaultValue) {
return getFormat(bytes, defaultValue);
}
}
Loading
Loading