5.8.5. Загрузка и вывод изображений
Рассмотрим задачу загрузки, хранения и отображения фотографий сотрудников:
-
Сотрудник представлен сущностью
Employee
. -
Файлы изображений хранятся в FileStorage. Сущность
Employee
содержит ссылку на соответствующийFileDescriptor
. -
Экран редактирования
Employee
отображает фотографию, а также дает возможность загрузить, выгрузить и очистить изображение.
Класс сущности со ссылкой на файл изображения:
@Table(name = "SAMPLE_EMPLOYEE")
@Entity(name = "sample$Employee")
public class Employee extends StandardEntity {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "IMAGE_FILE_ID")
protected FileDescriptor imageFile;
public void setImageFile(FileDescriptor imageFile) {
this.imageFile = imageFile;
}
public FileDescriptor getImageFile() {
return imageFile;
}
}
Представление для загрузки Employee
вместе с FileDescriptor
должно содержать все локальные атрибуты FileDescriptor
:
<view class="com.company.sample.entity.Employee"
name="employee-edit">
<property name="name"/>
...
<property name="imageFile"
view="_local">
</property>
</view>
Фрагмент XML-дескриптора экрана редактирования Employee
:
<groupBox caption="Photo" spacing="true"
height="250px" width="250px" expand="embeddedImage">
<embedded id="embeddedImage" width="100%"
align="MIDDLE_CENTER"/>
<hbox align="BOTTOM_LEFT"
spacing="true">
<upload id="uploadField"/>
<button id="downloadImageBtn"
caption="Download"
invoke="onDownloadImageBtnClick"/>
<button id="clearImageBtn"
caption="Clear"
invoke="onClearImageBtnClick"/>
</hbox>
</groupBox>
Компоненты отображения и загрузки/выгрузки фотографии заключены внутрь контейнера groupBox. В верхней его части с помощью компонента embedded выводится изображение, а в нижней слева направо расположены компонент upload для загрузки файла и кнопки выгрузки и очистки изображения. В результате эта часть экрана должна выглядеть следующим образом:
Теперь рассмотрим контроллер экрана редактирования.
public class EmployeeEdit extends AbstractEditor<Employee> {
private Logger log = LoggerFactory.getLogger(getClass());
@Inject
private DataSupplier dataSupplier;
@Inject
private FileStorageService fileStorageService;
@Inject
private FileUploadingAPI fileUploadingAPI;
@Inject
private ExportDisplay exportDisplay;
@Inject
private Embedded embeddedImage;
@Inject
private FileUploadField uploadField;
@Inject
private Button downloadImageBtn;
@Inject
private Button clearImageBtn;
@Inject
private Datasource<Employee> employeeDs;
private static final int IMG_HEIGHT = 190;
private static final int IMG_WIDTH = 220;
@Override
public void init(Map<String, Object> params) {
uploadField.addFileUploadSucceedListener(event -> {
FileDescriptor fd = uploadField.getFileDescriptor();
try {
fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd);
} catch (FileStorageException e) {
throw new RuntimeException("Error saving file to FileStorage", e);
}
getItem().setImageFile(dataSupplier.commit(fd));
displayImage();
});
uploadField.addFileUploadErrorListener(event ->
showNotification("File upload error", NotificationType.HUMANIZED));
employeeDs.addItemPropertyChangeListener(event -> {
if ("imageFile".equals(event.getProperty()))
updateImageButtons(event.getValue() != null);
});
}
@Override
protected void postInit() {
displayImage();
updateImageButtons(getItem().getImageFile() != null);
}
public void onDownloadImageBtnClick() {
if (getItem().getImageFile() != null)
exportDisplay.show(getItem().getImageFile(), ExportFormat.OCTET_STREAM);
}
public void onClearImageBtnClick() {
getItem().setImageFile(null);
displayImage();
}
private void updateImageButtons(boolean enable) {
downloadImageBtn.setEnabled(enable);
clearImageBtn.setEnabled(enable);
}
private void displayImage() {
byte[] bytes = null;
if (getItem().getImageFile() != null) {
try {
bytes = fileStorageService.loadFile(getItem().getImageFile());
} catch (FileStorageException e) {
log.error("Unable to load image file", e);
showNotification("Unable to load image file", NotificationType.HUMANIZED);
}
}
if (bytes != null) {
embeddedImage.setSource(getItem().getImageFile().getName(), new ByteArrayInputStream(bytes));
embeddedImage.setType(Embedded.Type.IMAGE);
BufferedImage image;
try {
image = ImageIO.read(new ByteArrayInputStream(bytes));
int width = image.getWidth();
int height = image.getHeight();
if (((double) height / (double) width) > ((double) IMG_HEIGHT / (double) IMG_WIDTH)) {
embeddedImage.setHeight(String.valueOf(IMG_HEIGHT));
embeddedImage.setWidth(String.valueOf(width * IMG_HEIGHT / height));
} else {
embeddedImage.setWidth(String.valueOf(IMG_WIDTH));
embeddedImage.setHeight(String.valueOf(height * IMG_WIDTH / width));
}
} catch (IOException e) {
log.error("Unable to resize image", e);
}
// refresh image
embeddedImage.setVisible(false);
embeddedImage.setVisible(true);
} else {
embeddedImage.setVisible(false);
}
}
}
-
В методе
init()
сначала инициализируется компонентuploadField
, предназначенный для загрузки новой фотографии. В случае успешной загрузки из компонента получается экземпляр новогоFileDescriptor
, и соответствующий файл отправляется из временного хранилища в постоянное вызовомFileUploadingAPI.putFileIntoStorage()
. После этогоFileDescriptor
сохраняется в БД вызовом DataSupplier.commit(), и сохраненный экземпляр устанавливается в атрибутеimageFile
редактируемой сущностиEmployee
. Затем вызывается методdisplayImage()
контроллера для отображения загруженной фотографии.Далее в методе
init()
источнику данных, содержащему редактируемый экземплярEmployee
, добавляется слушатель для запрещения или разрешения кнопок выгрузки и очистки файла в зависимости от того, загружен файл или нет. -
Метод
postInit()
вызывает отображение файла и обновляет состояние кнопок в зависимости от наличия загруженного файла. -
Метод
onDownloadImageBtnClick()
вызывается при нажатии кнопкиdownloadImageBtn
и выполняет выгрузку файла с помощью интерфейса ExportDisplay. -
Метод
onClearImageBtnClick()
вызывается при нажатии кнопкиclearImageBtn
и очищает атрибутimageFile
сущностиEmployee
. Удаления файла из хранилища не производится. -
Метод
displayImage()
выгружает файл из хранилища в байтовый массив, устанавливает содержимое компонентаembeddedImage
, и перерасчитывает его размеры для сохранения пропорций изображения.Следует иметь в виду, что выгрузка файлов из хранилища в байтовый массив приемлема только для небольших файлов. Если размер файла непредсказуем, следует использовать только выгрузку через ExportDisplay, при которой файл передается через потоки ввода-вывода и нигде не оказывается в памяти целиком.