Skip to content

Commit 0aaabd5

Browse files
authored
Merge pull request #3457 from ControlSystemStudio/CSSTUDIO-2906
Add check for HEIC file attachments
2 parents f67cf14 + d3004af commit 0aaabd5

File tree

14 files changed

+185
-9
lines changed

14 files changed

+185
-9
lines changed

app/logbook/olog/ui/doc/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ Here user may attach any number of files of arbitrary types:
7777
can be configured to use a different limit, but users should keep in mind that download of large attachments to
7878
the log viewer may incur delays in terms of UI updates.
7979

80+
**NOTE**: Since iOS 11 the default camera image format is HEIC/HEIF (High-Efficiency Image Format). This type of
81+
image file is not supported. Consequently upload of HEIC files is blocked by the application. Moreover, HEIC files converted to JPEG
82+
in native Mac OS applications (e.g. Preview) may also fail to render and are also blocked from upload.
83+
8084
Embedded images
8185
---------------
8286
Images may be embedded in the body text using markup. The user should consult the quick reference (Markup Help button)

app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public class Messages
5959
SelectFolder,
6060
ShowHideDetails,
6161
SizeLimitsText,
62+
UnsupportedFileType,
6263
UpdateLogEntry;
6364

6465
static

app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/AttachmentsEditorController.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@
4747
import org.phoebus.ui.docking.DockPane;
4848
import org.phoebus.ui.javafx.ImageCache;
4949
import org.phoebus.ui.javafx.Screenshot;
50+
import org.phoebus.util.MimeTypeDetector;
5051

5152
import javax.activation.MimetypesFileTypeMap;
5253
import javax.imageio.ImageIO;
5354
import java.io.File;
55+
import java.io.FileInputStream;
5456
import java.io.IOException;
5557
import java.text.MessageFormat;
5658
import java.util.ArrayList;
@@ -291,11 +293,18 @@ public void setTextArea(TextArea textArea) {
291293
this.textArea = textArea;
292294
}
293295

296+
/**
297+
* Add
298+
* @param files
299+
*/
294300
private void addFiles(List<File> files) {
295301
Platform.runLater(() -> sizesErrorMessage.set(null));
296302
if (!checkFileSizes(files)) {
297303
return;
298304
}
305+
if(!checkForHeicFiles(files)){
306+
return;
307+
}
299308
MimetypesFileTypeMap fileTypeMap = new MimetypesFileTypeMap();
300309
for (File file : files) {
301310
OlogAttachment ologAttachment = new OlogAttachment();
@@ -403,4 +412,43 @@ private void showTotalSizeExceedsLimit() {
403412
public void clearAttachments(){
404413
getAttachments().clear();
405414
}
415+
416+
/**
417+
* Checks if any of the attached files uses extension heic or heics, which are unsupported. If a heic file
418+
* is detected, an error dialog is shown.
419+
* @param files List of {@link File}s to check.
420+
* @return <code>true</code> if all is well, i.e. no heic files deyected, otherwise <code>false</code>.
421+
*/
422+
private boolean checkForHeicFiles(List<File> files){
423+
File file = detectHeicFiles(files);
424+
if(file != null){
425+
Alert alert = new Alert(AlertType.ERROR);
426+
alert.setHeaderText(MessageFormat.format(Messages.UnsupportedFileType, file.getAbsolutePath()));
427+
DialogHelper.positionDialog(alert, textArea, -200, -100);
428+
alert.show();
429+
return false;
430+
}
431+
return true;
432+
}
433+
434+
/**
435+
* Probes files for heic content, which is not supported.
436+
* @param files List of {@link File}s to check.
437+
* @return The first {@link File} in the list determined to have heic. If no such file
438+
* is detected, <code>null</code> is returned.
439+
*/
440+
private File detectHeicFiles(List<File> files){
441+
for(File file : files){
442+
try {
443+
String mimeType = MimeTypeDetector.determineMimeType(new FileInputStream(file));
444+
if(mimeType != null && mimeType.toLowerCase().contains("image/heic")){
445+
return file;
446+
}
447+
} catch (IOException e) {
448+
logger.log(Level.WARNING, "Cannot check file " +
449+
file.getAbsolutePath() + " for heic file content", e);
450+
}
451+
}
452+
return null;
453+
}
406454
}

app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/EmbedImageDialogController.java

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import java.io.File;
4646
import java.io.IOException;
4747
import java.net.URL;
48+
import java.util.List;
4849
import java.util.ResourceBundle;
4950
import java.util.UUID;
5051
import java.util.logging.Level;
@@ -55,49 +56,60 @@
5556
*/
5657
public class EmbedImageDialogController implements Initializable{
5758

59+
@SuppressWarnings("unused")
5860
@FXML
5961
private DialogPane dialogPane;
62+
@SuppressWarnings("unused")
6063
@FXML
6164
private Label fileLabel;
65+
@SuppressWarnings("unused")
6266
@FXML
6367
private Label widthLabel;
68+
@SuppressWarnings("unused")
6469
@FXML
6570
private Label heightLabel;
71+
@SuppressWarnings("unused")
6672
@FXML
6773
private Button browseButton;
74+
@SuppressWarnings("unused")
6875
@FXML
6976
private Button clipboardButton;
77+
@SuppressWarnings("unused")
7078
@FXML
7179
private TextField fileName;
80+
@SuppressWarnings("unused")
7281
@FXML
7382
private TextField width;
83+
@SuppressWarnings("unused")
7484
@FXML
7585
private TextField height;
86+
@SuppressWarnings("unused")
7687
@FXML
7788
private Label scaleLabel;
89+
@SuppressWarnings("unused")
7890
@FXML
7991
private TextField scale;
8092

8193
/**
8294
* This is set when image is selected from file or clipboard. It is not bound to
8395
* a UI component.
8496
*/
85-
private IntegerProperty widthProperty = new SimpleIntegerProperty();
97+
private final IntegerProperty widthProperty = new SimpleIntegerProperty();
8698
/**
8799
* This is the computed width value rendered in the UI component.
88100
*/
89-
private IntegerProperty scaledWidthProperty = new SimpleIntegerProperty();
101+
private final IntegerProperty scaledWidthProperty = new SimpleIntegerProperty();
90102
/**
91103
* This is set when image is selected from file or clipboard. It is not bound to
92104
* a UI component.
93105
*/
94-
private IntegerProperty heightProperty = new SimpleIntegerProperty();
106+
private final IntegerProperty heightProperty = new SimpleIntegerProperty();
95107
/**
96108
* This is the computed width value rendered in the UI component.
97109
*/
98-
private IntegerProperty scaledHeightProperty = new SimpleIntegerProperty();
99-
private SimpleStringProperty filenameProperty = new SimpleStringProperty();
100-
private DoubleProperty scaleProperty = new SimpleDoubleProperty(1.0);
110+
private final IntegerProperty scaledHeightProperty = new SimpleIntegerProperty();
111+
private final SimpleStringProperty filenameProperty = new SimpleStringProperty();
112+
private final DoubleProperty scaleProperty = new SimpleDoubleProperty(1.0);
101113

102114
private String id;
103115

@@ -127,13 +139,14 @@ public void initialize(URL url, ResourceBundle resourceBundle){
127139
dialogPane.lookupButton(ButtonType.OK).disableProperty().bind(okButtonBinding);
128140
}
129141

142+
@SuppressWarnings("unused")
130143
@FXML
131144
public void browse(){
132145
FileChooser fileChooser = new FileChooser();
133146
fileChooser.setTitle(Messages.SelectFile);
134147
fileChooser.getExtensionFilters().addAll(
135-
new FileChooser.ExtensionFilter("Image files (jpg, png, gif)", "*.jpg", "*.png", "*.gif"),
136-
new FileChooser.ExtensionFilter("All files", "*.*")
148+
new FileChooser.ExtensionFilter("Image files (jpg, jpeg, png, gif)", "*.jpg", "*.jpeg", "*.png", "*.gif")
149+
137150
);
138151

139152
File file = fileChooser.showOpenDialog(dialogPane.getScene().getWindow());
@@ -152,6 +165,7 @@ public void browse(){
152165
}
153166
}
154167

168+
@SuppressWarnings("unused")
155169
@FXML
156170
public void pasteClipboard(){
157171
image = Clipboard.getSystemClipboard().getImage();

app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Help=Help
5656
HitsPerPage=Hits per page:
5757
ImageWidth=Width
5858
ImageHeight=Height
59-
JumpToLogEntry=Jump to Log Entry:
59+
JumpToLogEntry=Jump to Log Entry:
6060
Level=Level
6161
Logbook=Logbook
6262
Logbooks=Logbooks:
@@ -118,6 +118,7 @@ Text=Text:
118118
Time=Time:
119119
Title=Title:
120120
UnableToDownloadArchived=
121+
UnsupportedFileType=Unsupported content found in file {0}
121122
Username=User Name:
122123
UpdateLogEntry=Edit
123124
HtmlPreview=Preview

core/util/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
<artifactId>javax.ws.rs-api</artifactId>
1414
<version>2.1</version>
1515
</dependency>
16+
<!-- https://mvnrepository.com/artifact/org.apache.tika/tika-core -->
17+
<dependency>
18+
<groupId>org.apache.tika</groupId>
19+
<artifactId>tika-core</artifactId>
20+
<version>3.2.0</version>
21+
</dependency>
1622
<dependency>
1723
<groupId>org.junit.jupiter</groupId>
1824
<artifactId>junit-jupiter</artifactId>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright (C) 2025 European Spallation Source ERIC.
3+
*/
4+
5+
package org.phoebus.util;
6+
7+
import org.apache.tika.Tika;
8+
import org.apache.tika.metadata.Metadata;
9+
10+
import java.io.IOException;
11+
import java.io.InputStream;
12+
import java.util.logging.Level;
13+
import java.util.logging.Logger;
14+
15+
public class MimeTypeDetector {
16+
17+
/**
18+
* Attempts to determine the file type based on its content. Cleverness delegated to Apache Tika.
19+
* <p>
20+
* Note that the {@link InputStream} will be closed by this method.
21+
* </p>
22+
*
23+
* @param inputStream A non-null {@link InputStream} that will be closed by this method.
24+
* @return A MIME type string, e.g. image/jpeg
25+
* @throws IOException If there is a problem reading the stream or if the provided {@link InputStream} is
26+
* <code>null</code>.
27+
*/
28+
public static String determineMimeType(InputStream inputStream) throws IOException {
29+
if (inputStream == null) {
30+
throw new IOException("InputStream must not be null");
31+
}
32+
try {
33+
return new Tika().detect(inputStream, new Metadata());
34+
} catch (IOException e) {
35+
Logger.getLogger(MimeTypeDetector.class.getName())
36+
.log(Level.WARNING, "Unable to read input stream", e);
37+
throw e;
38+
} finally {
39+
try {
40+
inputStream.close();
41+
} catch (IOException e) {
42+
Logger.getLogger(MimeTypeDetector.class.getName())
43+
.log(Level.WARNING, "Failed to close input stream", e);
44+
}
45+
}
46+
}
47+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright (C) 2025 European Spallation Source ERIC.
3+
*/
4+
5+
package org.phoebus.util;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
import java.io.IOException;
10+
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
import static org.junit.jupiter.api.Assertions.assertThrows;
13+
14+
public class MimeTypeDetectorTest {
15+
16+
@Test
17+
public void testHeic() throws Exception {
18+
assertEquals("image/heic",
19+
MimeTypeDetector
20+
.determineMimeType(MimeTypeDetector.class.getResourceAsStream("ios_original.HEIC")));
21+
22+
assertEquals("image/heic",
23+
MimeTypeDetector
24+
.determineMimeType(MimeTypeDetector.class.getResourceAsStream("ios_heic_converted.JPEG")));
25+
}
26+
27+
@Test
28+
public void testJpeg() throws Exception {
29+
assertEquals("image/jpeg",
30+
MimeTypeDetector
31+
.determineMimeType(MimeTypeDetector.class.getResourceAsStream("proper_jpeg.jpeg")));
32+
}
33+
34+
@Test
35+
public void testPdf() throws Exception {
36+
assertEquals("application/pdf",
37+
MimeTypeDetector
38+
.determineMimeType(MimeTypeDetector.class.getResourceAsStream("mimetest.pdf")));
39+
}
40+
41+
@Test
42+
public void testNullInputStream() {
43+
assertThrows(IOException.class, () ->
44+
MimeTypeDetector
45+
.determineMimeType(MimeTypeDetector.class.getResourceAsStream("does_no_exist")));
46+
}
47+
}
3.52 MB
Loading
3.58 MB
Binary file not shown.

0 commit comments

Comments
 (0)