Accessing Files & File Handler – Real-World PWA: The Making Of Paint.Js.Org – Part 4

In this fourth part of the series about the Microsoft Paint remake on paint.js.org, I want to demonstrate how you can save your drawings to your local disk, read them back later and how to add your web app as a handler for certain file extensions.

In diesem Artikel:

Accessing Files & File Handler – Real-World PWA: The Making Of Paint.Js.Org – Part 4
Christian Liebel ist Consultant bei Thinktecture. Sein Fokus liegt auf Webstandards und Progressive Web Apps. Er vertritt Thinktecture beim W3C.

Paint is a traditional productivity app using a file-based workflow: Typically, you create a new drawing within the application and save it to the local file system. To open the drawing later again, you could locate the file using your favorite file browser and double-click it to bring Paint back up. That’s the classic workflow that users of desktop software are used to for decades. With the help of modern web APIs introduced by Project Fugu, web apps can now also integrate into this workflow.

Safe and Secure File System Access

With Google Chrome 86 (and many other Chromium-based browsers starting from the same version), the File System Access API was made generally available. This API allows developers to interact with the local file system while ensuring the user’s security and privacy. It’s not an official W3C Recommendation yet, but a draft report by the Web Incubator Community Group (WICG), the right place to discuss new APIs with other developers and browser vendors.

Among others, the API introduces two new methods on the global window object, showSaveFilePicker() and showOpenFilePicker() for saving and opening files from the local file system. To keep the API secure, those methods are only exposed for secure origins (i.e., websites delivered via HTTPS). They can only be called as a part of a user gesture (i.e., a click or key press) and only originate from the top-level document (i.e., not from a nested iframe). Furthermore, the user has to explicitly consent to the operation by selecting the target files at least once via the file picker. The API introduces a file handle that can be used to overwrite existing files, a feature that was unavailable on the web platform before. Also, the API allows dealing with files that are dragged into the browser window or tab and listing and modify directory contents. For more details, I recommend this web.dev article on the File System Access API.

At the time of this writing, Chromium-based browsers are the only ones supporting this API. Also, it is only available on desktop builds, as mobile devices typically don’t expose the file system directly to the user.

Stop Worrying about Platforms with Browser-fs-Access

Fortunately, the web platform offers a lot of fallback mechanisms to achieve similar things: On the one hand, the input[type=file] element allows you to select a file from the local file system using a file open picker. There’s even a WebKit-proprietary property (webkitdirectory) to enable the selection of directories instead of files. On the other hand, the a[download] element allows you to directly download a file (including dynamically generated blobs) to your Downloads folder. These fallback methods are available on a variety of platforms, both desktop, and mobile. Therefore, it makes sense to use a ponyfill (i.e., a polyfill that doesn’t monkey-patch global APIs) that uses the File System Access API if available and automatically falls back to an alternative approach if it isn’t. For example, there is the project browser-fs-access, a Google experiment maintained by Thomas Steiner, which I use in the Paint remake.

Saving and Opening Files

Implementing the file operations is easy: To show a file open dialog, you simply call the fileOpen() method provided by the ponyfill. It takes an array of compatible extensions, media types, and a description. The library takes care of detecting whether File System Access API is available and otherwise invokes a fallback method. This is how the Paint remake invokes the file open dialog:

				
					const file = await fileOpen({
  extensions: ['.png'],
  description: 'PNG Files',
});
drawingContext.document.handle = file.handle;
drawingContext.document.title = file.name;
updateContext(drawingContext.element);

await loadFileAndAdjustCanvas(file, drawingContext);
				
			

At the time of this writing, the Paint remake only allows selecting PNG files. If the user successfully selected a file from within the file picker, the returned promise resolves and returns a File object. However, if the user canceled the operation, the promise rejects, which could be caught in a try block.

The returned object contains a handle property if the File System Access API is supported, which can be used to overwrite the opened file later on. This property would be undefined if a fallback mechanism was used. The name property contains the file’s name. Both properties are set to Paint’s document context. For instance, the current file name is shown in the title bar of the application. Next, the loadFileAndAdjustCanvas() method is called. This method takes care of extracting the pixel data from the PNG binary format and adjusting the boundaries of the canvas accordingly.

Saving a file works similar, but the other way round: First, you need to convert the canvas’s pixel data to a binary format and then let the user pick a target file to write the binary data to. This is done by using the fileSave() method of browser-fs-access. It takes the binary data and a configuration object similar to the one from fileOpen(): In the save file picker, we also only want to offer PNG files. In contrast to the file open options, you can also specify a suggested file name that appears in the file picker as the default file name. The following code shows the Paint remake’s Save as implementation:

				
					const blob = await toBlob(canvas);
const file = await fileSave(blob, {
  fileName: document.title,
  extensions: ['.png'],
  description: 'PNG files',
});
if (file) {
  document.title = file.name;
  document.handle = file;
  updateContext(element);
}
				
			

If saving was successful and the File System Access API is available, you retrieve the handle of the saved file. In this case, the document context (including the title in the window bar) will be updated again.

If the File System Access API is supported and you have a file handle, you can also overwrite an existing file by passing the handle to the fileSave() method as a third parameter. This is how the Paint remake implements the Save menu item (in case a handle is present):

				
					await fileSave(blob, undefined, drawingContext.document.handle);
				
			

If no handle is present, the menu action calls the Save as routine.

Register Your PWA as a File Handler

Let’s say you now want to double-click an existing file and bring up your Progressive Web App again. The File Handling API, also discussed at WICG, allows exactly that: For registering your web app as a file handler, it’s required that the user installs it so that there is a wrapper with an icon and text that the operating system can show. To register your PWA as a file handler, add the file_handlers property to your Web Application Manifest like so:

				
					{
  "file_handlers": [{
    "action": "/",
    "accept": {
      "image/png": [".png"]
    }
  }]
}
				
			

The property takes an array that can contain different file handler objects. Each object has an action, representing the URL that is invoked when a file is opened. In the accept object, you enumerate the media types and file extensions that your app is compatible with. Please note that there’s no option to configure your application as the default file handler. If there are no other apps registered for the target extension, your application will become the default handler, but if there are other applications, the default app will not change. After declaring support for handling one or more filetypes and after installing the PWA to your system, the app will now appear in the list of compatible programs for files with the type according:

That’s the first part of the implementation. Next, we need to imperatively react to when the user opened our PWA from double-clicking a file. The PWA will be opened using the URL from the action property. Using the launchQueue from the global window object, the application can listen to the incoming parameters by calling the setConsumer() method. On the params object, you can find a files property that contains the list of file handles the application was opened with. From there, the Paint remake will load the binary contents of the file, and update the document context just as for the Open action:

				
					window.launchQueue.setConsumer(async (params) => {
  const [handle] = params.files;
  if (handle) {
    const file = await handle.getFile();
    drawingContext.document.title = file.name;
    drawingContext.document.handle = handle;
    await loadFileAndAdjustCanvas(file, drawingContext);
  }
});
				
			

In order to ensure the user’s security and privacy, the browser will ask for the user’s permission the first time you open a certain PWA using this way. Please note that, at the time of this writing, the File Handling API is in an early state. It’s only present in Chromium-based browsers, and users have to enable the #file-handling-api flag in order to activate the API. The API is scheduled for origin trial in Chromium versions 92—94, and if there are no major blockers, the API could ship soon thereafter.

Summary

By combining File System Access API (shipped with Chromium 86) and the upcoming File Handling API, PWAs can finally integrate into the same file-based workflow traditional productivity applications do. This enables a whole class of desktop apps to make the shift to the web finally. The Paint remake is a great demonstration that this works absolutely flawless.

In the next part of this series, I want to add another native feature to the remake: Dark mode. Especially during nighttime, a light-on-dark color scheme is easier on the eyes.

Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Diese Artikel könnten Sie interessieren
Project Fugu
Dark Mode Support – Real-World PWA: The Making Of Paint.Js.Org – Part 5

Dark Mode Support – Real-World PWA: The Making Of Paint.Js.Org – Part 5

In part five of the series about the making of the web-based Microsoft Paint clone paint.js.org, I want to show how to implement support for dark mode in your web applications.
20.05.2021
Project Fugu
Copy & Paste Images – Real-World PWA: The Making Of Paint.Js.Org – Part 3

Copy & Paste Images – Real-World PWA: The Making Of Paint.Js.Org – Part 3

In part three of the series about the making of the web-based Microsoft Paint clone paint.js.org, I want to show how you can copy drawings from the Paint clone to other applications and paste them back.
27.04.2021
Project Fugu
Canvas & Input – Real-World PWA: The Making Of Paint.Js.Org – Part 2

Canvas & Input – Real-World PWA: The Making Of Paint.Js.Org – Part 2

After introducing into the project about the web-based Microsoft Paint clone in the first part of this series and talking about the choice of Web Components and the architecture of paint.js.org, I now want to demonstrate how I implemented the drawing functionality.
15.04.2021
Project Fugu
Overview, Web Components & Architecture – Real-World PWA: The Making Of Paint.Js.Org – Part 1

Overview, Web Components & Architecture – Real-World PWA: The Making Of Paint.Js.Org – Part 1

Progressive Web Apps and the new powerful web APIs provided by Project Fugu allow developers to implement desktop-class productivity apps using web technologies. In this six-part article series, Christian Liebel shows you the critical parts of how paint.js.org was made, a web-based clone of the productivity app dinosaur Microsoft Paint. In this first article, Christian gives you an overview of the project, explains the choice of Web Components, and discusses the basic app architecture of the web-based Microsoft Paint clone.
07.04.2021
Angular
From an Angular Web App to a Hybrid App with Capacitor and Ionic Framework

From an Angular Web App to a Hybrid App with Capacitor and Ionic Framework

Creating mobile and desktop applications is a complicated task. Different operating systems require us to have in-depth knowledge of multiple platforms. The web solves most of the problems for us by providing a single platform that runs everywhere - supported by different operating systems and browser vendors. Frameworks like Angular bring the power to write business applications for the web and make a once written application available to everyone.
10.03.2021
Angular
Additional Approaches: Advanced Progressive Web Apps – Push Notifications Under Control – Part 4

Additional Approaches: Advanced Progressive Web Apps – Push Notifications Under Control – Part 4

In the previous parts of this article series, we learned that Apple does not support the standardized web-based push mechanisms, and there is no sign of a possible timeline for implementation. Therefore we have to look at additional ways to bring the users' attention back to our application. Let's use the final article of the series to have a quick look at some approaches that will let us send some form of push message without using the Push API.
15.10.2020