Sometimes, your Angular app can grab images the "old-fashioned way" with a simple URL. But sometimes it can't.

In those cases when it can't, you might need to get the image with a REST service. But how do you do that?

It's easier than you think.

In this guide, I'll show you how to grab images from a REST service and display them in your Angular app.

If you'd rather just get the code, you can do so on GitHub (here for the Spring Boot service and here for the Angular code). Otherwise, pull up a rock and sit a while.

Remember: if you're having problems running the Angular app locally, be sure to check the README.

Please note: this guide is a part of an ongoing series of tutorials on how to create a customer relationship management (CRM) application from scratch. The GitHub code for each guide belongs to a specific branch. The master branch holds the entire application up to the current date. If you want to follow the whole series, just view the careydevelopmentcrm tag. Note that the website displays the guides in reverse chronological order so if you want to start from the beginning, go to the last page.

The Business Requirements

Your boss Smithers walks into your office, sits down, and sighs.

"It's about that CRM app you're working on," he says. "That profile photo upload thing only allows users to upload a new photo. It doesn't display the photo they've already uploaded."

You begin a reply: "Okay, well would y-"

Smithers holds up his hand, effectively shushing you.

"Just update the code so the user can see a photo that's already been uploaded. Then the user can decide if it needs to be replaced," he says.

He tells you he needs to go to the bathroom and walks out of your office.

Back End (The High Life Again)

For this requirement, you're going to need to make some changes to the back end as well as the front end.

For starters, make some updates to FileUtil. As you might recall, that class includes several utility methods that make it easy for you to add new files on the server's operating system.

Here are the changes you need to make:

    public Path fetchProfilePhotoByUserId(String userId) throws ImageRetrievalException {
        Path imagePath = null;
        
        Path rootLocation = Paths.get(getRootLocationForUserProfileImageUpload(userId));
        LOG.debug("Fetching profile image from " + rootLocation.toString());

        try {
            if (rootLocation.toFile().exists()) {
                Iterator<Path> iterator = Files.newDirectoryStream(rootLocation).iterator();
                
                if (iterator.hasNext()) {
                    imagePath = iterator.next();                
                    LOG.debug("File name is " + imagePath);
                }            
            }
        } catch (IOException ie) {
            throw new ImageRetrievalException(ie.getMessage());
        }
        
        return imagePath;
    }
    
    
    private void deleteAllFilesInDirectory(Path rootLocation) {
        try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(rootLocation)) {
            directoryStream.forEach(path -> {
                path.toFile().delete();
            });
        } catch (IOException ie) {
            LOG.error("Problem trying to delete files in " + rootLocation.toString());
        }
    }
    
    
    public void saveProfilePhoto(MultipartFile file, User user) throws MissingFileException, FileTooLargeException, CopyFileException {
        validateFile(file, maxFileUploadSize);
        Path rootLocation = Paths.get(getRootLocationForUserProfileImageUpload(user));
        deleteAllFilesInDirectory(rootLocation);
        saveFile(file, user, rootLocation);
    }

    
    private void saveFile(MultipartFile file, User user, Path rootLocation) throws CopyFileException {
        try (InputStream is = file.getInputStream()) {
            String newFileName = getNewFileName(file, user);
            Files.copy(is, rootLocation.resolve(newFileName));
        } catch (IOException ie) {
            LOG.error("Problem uploading file!", ie);
            throw new CopyFileException("Failed to upload!");
        }
    }

The first method, fetchProfilePhotoByUserId(), does exactly what you think it does. It goes out to the operating system and finds the user's profile photo.

The method returns the file as a Path object. If the file doesn't exist, it returns null.

Remember, from a previous guide, that profile photos get stored in the following directory path:

/etc/careydevelopment/users/[user ID]/profile

There should only be at most one image in that folder. At least, that's how it will be after you're done with this guide.

The next method, deleteAllFilesInDirectory(), is fairly self-explanatory.

The saveProfilePhoto() method is an update to the old saveFile() method. It's got a better name and now also uses the previous method to delete any old photos in the user's profile folder.

The saveFile() method is more generic. It handles all file uploads regardless of whether they're profile photos or something else.

For the other methods in that class, check out the source on GitHub.

Winners & Users

Now it's time for a new controller: UserController.

package com.careydevelopment.ecosystem.user.controller;

import java.nio.file.Files;
import java.nio.file.Path;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.careydevelopment.ecosystem.user.util.FileUtil;
import com.careydevelopment.ecosystem.user.util.SecurityUtil;

@CrossOrigin(origins = "*")
@RestController
public class UserController {
    
    private static final Logger LOG = LoggerFactory.getLogger(UserController.class);

    
    @Autowired
    private SecurityUtil securityUtil;
    
    @Autowired
    private FileUtil fileUtil;
    
    
    @GetMapping("/user/{userId}/profileImage")
    public ResponseEntity<?> getProfileImage(@PathVariable String userId) {        
        try {
            Path imagePath = fileUtil.fetchProfilePhotoByUserId(userId);
            
            if (imagePath != null) {
                LOG.debug("Getting image from " + imagePath.toString());
                
                ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(imagePath));
                
                return ResponseEntity
                        .ok()
                        .contentLength(imagePath.toFile().length())
                        .contentType(MediaType.IMAGE_JPEG)
                        .body(resource);                    
            } else {
                LOG.debug("Profile photo not found for user " + userId);
                return ResponseEntity.status(HttpStatus.OK).build();
            }
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

So far, there's only one method in that controller. Don't worry, that will change over time.

The single method, getProfileImage(), uses the aforementioned FileUtil to grab the user's profile photo.

It does that by examining the user ID passed in the URL path ("/user/{userId}/profileImage"). Then, it retrieves the profile photo for that user.

If the file isn't null, it sends the file's byte array back to the calling client.

Note that the code still returns a 200 (OK) response even if the file isn't found. That's intentional.

I don't think it's a good idea to return an error condition if the file isn't found. Instead, the code just returns an empty bytestream and the client can act accordingly.

The Client by John Grisham

That takes care of the server-side code. Now it's time to muck about in the client-side source.

Please note: if you're following along with these guides, I've made some client-side changes that won't be covered here. Make sure you get all the latest source.

The part of the code that Smithers took issue with is located in /src/app/features/user/profile-image. So that's where you'll need to make some changes.

Before you do that, though, you need to update UserService.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from '../../models/user';

@Injectable({ providedIn: 'root' })
export class UserService {

  private _user: User = null;

  constructor(private http: HttpClient) { }

  get user(): User {
    return this._user;
  }

  set user(user: User) {
    this._user = user;
  }

  fetchProfileImage(userId: string): Observable<Blob> {
    let url = "http://localhost:8080/user/" + userId + "/profileImage";
    console.log("Profile image URL is " + url);

    return this.http.get(url, { responseType: 'blob' });
  }
}

The service now caches the User object. That way, the app doesn't have to repteatedly go out to the database to get user info.

Any object that injects the UserService dependency can just use the service to get all the user details. 

Now take a look at fetchProfileImage(). That method uses HttpClient to go to the downstream user service and fetch the profile image. 

It uses the user ID to create a URL that corresponds to the format you saw when you looked at the controller in a previous section. Then, it performs a GET request to grab the image.

Note that the Observable returned here uses a Blob type parameter. You'll also see the word "blob" as the response type associated with the GET.

So... what the heck is a blob?

It stands for Binary Large OBject. In this case, the code is telling the Angular framework to expect a binary response instead of a text response from the downstream service.

And, indeed, that's exactly what the service is returning. Go take another peek at the controller for evidence of that.

Image Marketing

Now it's time to update ProfileImageComponent. Here are the changes you need to make:

  currentFileUpload: UploadedImage;
  changeImage: boolean = false;
  uploading: boolean = false;
  imageToShow: any = null;
  user: User = null;
  showSpinner: boolean = true;

  constructor(private uploadService: UploadFileService, private userService: UserService,
    private imageService: ImageService, private alertService: AlertService) { }

  ngOnInit() {
    this.user = this.userService.user;

    this.userService.fetchProfileImage(this.user.id)
      .subscribe(image => this.createImage(image),
        err => this.handleImageRetrievalError(err));
  }

  private handleImageRetrievalError(err: Error) {
    console.error(err);
    this.showSpinner = false;
    this.alertService.error("Problem retrieving profile photo.");
  }

  private createImage(image: Blob) {
    if (image && image.size > 0) {
      let reader = new FileReader();

      reader.addEventListener("load", () => {
        this.imageToShow = reader.result;
        this.showSpinner = false;
      }, false);

      reader.readAsDataURL(image);
    } else {
      this.showSpinner = false;
    }
  }

First, take note of a few new fields.

The imageToShow field is the raw binary that contains the user's profile photo. It starts off as null.

The next field, user, is the User object that contains the current user's details. The code uses that object to get the user ID and then uses the ID to get the profile photo.

The next field, showSpinner, is a boolean that tells the application whether or not to show the spinner.

What spinner? The one that users will see while the system is loading the profile photo. You can learn more about Angular Material spinners from a previous guide.

The ngOnInit() method starts by grabbing the User object from UserService. Then it uses the user ID to fetch the profile image.

The handleImageRetrievalError() method is fairly self-explanatory. It just logs whatever error occurred, stops the spinner, and moves on.

The createImage() method, on the other hand, requires a little more explanation.

First, it checks to make sure an image actually came back. If not, it stops the spinner and moves on.

If there is an image, though, it instantiates a fresh FileReader object. It uses that object to read the binary contents returned by the downstream service. 

Since that whole read process takes some time, the code adds an event listener to the FileReader. The event listener waits for the data to load and sets imageToShow as the image that the HTML will display to the user.

Marked Up

Now take a look at the HTML associated with the component. Edit profile-image.component.html.

<div fxFlex fxLayout="column" fxLayoutGap="0px" class="route-content">
  <div fxLayout="row wrap">
    <div fxLayout="column">
      <div>
        <h4>Profile Image</h4>
        <p>Upload a square image no smaller than 200x200. Then click the <strong>Save Image</strong> button to save it.</p>
      </div>
      <div class="alert-section">
        <alert></alert>
      </div>
      <div class="upload-image-box">
        <div style="text-align: center">
          <app-image-uploader [showSpinner]="showSpinner" [image]="imageToShow"
                              (uploadedImage)="onUploadedImage($event)"></app-image-uploader>
        </div>
        <div *ngIf="!uploading" style="text-align:center">
          <button mat-raised-button color="primary" [disabled]="!currentFileUpload" (click)="upload()">Save Image</button>
        </div>
        <div *ngIf="uploading">
          <mat-spinner [diameter]="50" class="center-div"></mat-spinner>
        </div>
      </div>
    </div>
  </div>
</div>

Pay close attention to the <app-image-uploader> element. It uses a class called ImageUploaderComponent that I explaned in my guide on image uploads.

However the <app-image-uploader> element uses two new properties not seen in that guide: [showSpinner] and [image]

The [showSpinner] property maps to the component boolean of the same name. I covered it in the previous section.

The [image] property maps to the imageToShow field in the component. 

So here's what's going on: if showSpinner is true, the app will show a spinner while it's waiting for the profile photo to come back from the downstream service.

Once that profile photo is returned, the ImageUploaderComponent will show the profile photo instead of the spinner.

If no profile photo exists, the ImageUploaderComponent will show the default image. That's currently a large "upload" icon.

You can check out how the component does that by reviewing the relevent component source. Be sure to go over the related HTML as well.

Tryout

Okay, now it's time to test this thing to make sure it works as expected.

Start the Spring Boot application you worked on in the beginning of this guide. Then launch your Angular app.

Head over to http://localhost:4200/login and login with the usual credentials (darth/thedarkside).

Once you get to the dashboard, go to User and Profile Image. If you already have a profile image saved, it should appear right away.

 

(Yes, I changed the look and feel of the profile photo upload page again. I'm still looking for "my groove.")

Now try another experiment. Go to the operating system and delete your profile photo.

Remember, it's located in \etc\careydevelopment\users\[Your User ID]\profile

Then go back to the app. Navigate to the dashboard and then to the profile image upload page. You should see the default image.

 

Now pick a profile photo and upload it. Then, once again, navigate away from the page and come back. Once again, you should see the image you just uploaded.

Wrapping It Up

And there you have it. You now have a way to fetch and display images in your Angular app via a REST service.

Now it's your turn. Try some refactoring. There's plenty of opportunity for it in the current code.

Also, look for ways to improve the look and feel of the UI.

Remember, you can always grab the source code for the Spring Boot service or the UI at any time.

Have fun!

Photo by Ryutaro Tsukata from Pexels