Having difficulties with downloading an image as a zip file in Angular 6

Here I have the following dynamic data:

data =  [
  "https://dummyimage.com/200x200/000/fff.jpg&text=test", 
  "https://dummyimage.com/200x200/000/fff.jpg&text=testOne",
  "https://dummyimage.com/200x200/000/fff.png&text=testTwo"
]

When a button is clicked, I want to retrieve all the images from these URLs and save them as a zip file.

Problem: After downloading the file as a zip and trying to extract it, an error occurs stating that it could not open "image.zip" as an archive. Even when saving as a single image, the image does not seem to open properly. Is there any way to resolve this issue?

Below is my code snippet for downloading image data:

downloadImageData(){
  var blob = new Blob([this.data], { type:  'application/zip' });
  FileSaver.saveAs(blob,'image.zip');
}

The data contains both PNG and JPG formats, so regardless of the data type retrieved via the links, it should be downloaded as a zip file. Are there any strategies or approaches for achieving this in Angular 5+? I am currently using the FileSaver angular package.

JS ZIP _body response:

Upon utilizing the http module, the below data is obtained:

  {
    "_body": {

    },
    "status": 200,
    "ok": true,
    "statusText": "OK",
    "headers": {
      "date": [
        "Sun",
        " 25 Nov 2018 12:18:47 GMT"
      ],
      "cache-control": [
        "public",
        " max-age=43200"
      ],
      "expires": [
        "Mon",
        " 26 Nov 2018 00:18:47 GMT"
      ],
      "content-disposition": [
        "attachment; filename=2B.JPG"
      ],
      "content-length": [
        "40649"
      ],
      "server": [
        "Werkzeug/0.14.1 Python/2.7.13"
      ],
      "content-type": [
        "image/jpg"
      ]
    },
    "type": 2,
    "url": "http://some url"
  }
]

Answer №1

If you're looking to experiment with a demo application, check out the link here.

Just a heads up: The code provided is meant for reference purposes and may not adhere to standard coding practices, but it can certainly steer you in the right direction towards crafting your own solution.

One key component utilized in this project is jszip, essential for zipping files.

app.module.ts:

A snippet from the module implementation:
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent }  from './app.component';
import * as JSZip from 'jszip';
import { saveAs } from 'file-saver';

@NgModule({
imports: [ BrowserModule, HttpClientModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

app.component.ts:

An overview of the component details:
import { OnInit, Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import * as JSZip from 'jszip';
import { saveAs } from 'file-saver';

@Component({
selector: 'my-app',
template: `<button (click)='downloadZip()'>Download</button>`
})
export class AppComponent {

constructor(private http: HttpClient) {
}

downloadZip() {
this.loadSvgData("https://c.staticblitz.com/assets/client/icons/file-icons/angular-component-31179578a9a8a16512e9e90ade26549a.svg",
this.saveAsZip);
}

private loadSvgData(url: string, callback: Function) : void{
this.http.get(url, { responseType: "arraybuffer" })
.subscribe(x => callback(x));
}

private saveAsZip(content: Blob) : void{
var zip = new JSZip.default();
zip.file("image.svg", content);
zip.generateAsync({ type: "blob" })
.then(blob => saveAs(blob,'image.zip'));
};
}

Explanation:

The application essentially comprises a single button that triggers the download of an image file from the server through HttpClient. Post-download, this data is compressed using jszipand promptly stored on the browser utilizing file-saver.

Hopefully, this breakdown provides some clarity!

Answer №2

I may be running a bit behind, but this code snippet is designed to utilize your image array and generate GET requests accordingly. It will then proceed to execute all requests, add the responses to your zip file, and facilitate the download process.

I've included two options for downloading the file in case you're not keen on using the fileSaver. Feel free to choose whichever method suits you best.

EDIT:

If you happen to be utilizing an older version of rxjs, note that importing forkJoin might require a different approach. Refer to the rxjs documentation for further guidance. Additionally, ensure that your backend permits file downloads to prevent CORS errors.

forkJoin Documentation

app.component.ts

import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { forkJoin } from "rxjs";
import { saveAs } from "file-saver";
import * as JSZip from 'jszip';

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {

  data = [
    'http://yoururl/file.png',
    'http://yoururl/file2.png'
  ];

  getRequests = [];

  constructor(private _http: HttpClient) {}

  download() {
    this.createGetRequets(this.data);

    forkJoin(...this.getRequests)
     .subscribe((res) => {
      const zip = new JSZip();

      res.forEach((f, i) => {
        zip.file(`image${i}.png`, f);
      });

      /* With file saver */
      // zip
      //   .generateAsync({ type: 'blob' })
      //   .then(blob => saveAs(blob, 'image.zip'));

      /* Without file saver */
      zip
        .generateAsync({ type: 'blob' })
        .then(blob => {
          const a: any = document.createElement('a');
          document.body.appendChild(a);

          a.style = 'display: none';
          const url = window.URL.createObjectURL(blob);
          a.href = url;
          a.download = 'image.zip';
          a.click();
          window.URL.revokeObjectURL(url);
        });
     });
  }

  private createGetRequets(data: string[]) {
    data.forEach(url => this.getRequests.push(this._http.get(url, { responseType: 'blob' })));
  }
}

app.component.html

<div style="text-align:center">
  <button (click)="download()">Download</button>
</div>

In order to integrate jszip into my application, I had to update tsconfig.json with the path reference. The necessity of this step may vary based on your Angular version. Within "compilerOptions", include the following:

tsconfig.json

"paths": {
      "jszip": [
        "node_modules/jszip/dist/jszip.min.js"
      ]
    }

UPDATE:

For those still leveraging the old HttpModule, the provided solution remains functional. However, transitioning to the newer HttpClientModule is advisable if feasible.

UPDATE2:

To accommodate various file types, consider altering the file extension when saving the downloaded files. This can enhance the versatility of the solution.

app.component.ts

import { Component } from "@angular/core";
import { Http, ResponseContentType } from "@angular/http"; // Different Import
import { forkJoin } from "rxjs";
import { saveAs } from "file-saver";
import * as JSZip from "jszip";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {

  /* 
    UPDATE 2
    Create a Type map to handle differnet file types 
  */
  readonly MIME_TYPE_MAP = {
    "image/png": "png",
    "image/jpeg": "jpg",
    "image/jpg": "jpg",
    "image/gif": "gif"
  };

  data = [
    "http://url/file.png",
    "http://url/file.jpeg",
    "http://url/file.gif"
  ];

  getRequests = [];

  constructor(private _http: Http) {} // Different Constructor

  download() {
    this.createGetRequets(this.data);

    forkJoin(...this.getRequests).subscribe(res => {
      const zip = new JSZip();
      console.log(res);
      /*
        The return value is different when using the HttpModule.
        Now you need do access the body of the response with ._body,
        as you can see inside the forEach loop => f._body
      */
      let fileExt: String;  // UPDATE 2

      res.forEach((f, i) => {
        fileExt = this.MIME_TYPE_MAP[f._body.type]; // UPDATE 2, retrieve type from the response.
        zip.file(`image${i}.${fileExt}`, f._body);  // UPDATE 2, append the file extension when saving
      });

      zip
        .generateAsync({ type: "blob" })
        .then(blob => saveAs(blob, "image.zip"));
    });
  }

  private createGetRequets(data: string[]) {
    /*
      Change your responseType to ResponseContentType.Blob
    */
    data.forEach(url =>
      this.getRequests.push(
        this._http.get(url, { responseType: ResponseContentType.Blob })
      )
    );
  }
}

UPDATE3:

To simplify handling of file types, extracting the file name from the URL can obviate the need for specifying file types individually. Here's an alternative approach:

import { Component } from "@angular/core";
import { Http, ResponseContentType } from "@angular/http";
import { forkJoin } from "rxjs";
import { saveAs } from "file-saver";
import * as JSZip from "jszip";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  data = ["http://url/file.png", "http://url/file.jpg", "http://url/file.gif"];

  getRequests = [];

  constructor(private _http: Http) {}

  download() {
    this.createGetRequets(this.data);

    forkJoin(...this.getRequests).subscribe(res => {
      const zip = new JSZip();
      let fileName: String;

      res.forEach((f, i) => {
        fileName = f.url.substring(f.url.lastIndexOf("/") + 1); // extract filename from the response
        zip.file(`${fileName}`, f._body); // use it as name, no need for explicit file type declaration
      });

      zip
        .generateAsync({ type: "blob" })
        .then(blob => saveAs(blob, "image.zip"));
    });
  }

  private createGetRequets(data: string[]) {
    data.forEach(url =>
      this.getRequests.push(
        this._http.get(url, { responseType: ResponseContentType.Blob })
      )
    );
  }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Increasing response buffer size in Node.js fetch for version 2.x.x

Currently in the process of implementing an API request using nodejs-fetch and I've encountered an issue. The documentation states that the maximum buffer size for fetch is 16kB, but the response I need to retrieve is 53 kB. This causes the .fetch() f ...

Manage and preserve your node.js/express sessions with this offer

Currently, I am working on a web application that encounters an issue where every time fs.mkdir is called, all the current express sessions are deleted. This causes me to lose all session data and I need a solution to keep these sessions intact. I have att ...

Angular - Expanding a d3 SVG element generated dynamically in the code

Welcome to my first question on this platform. If I make any mistakes, please forgive me. I am currently working on creating a simple SVG in Angular using the d3 library, but I am struggling to make it scale properly. After doing extensive research on SV ...

Steps to update the text of the button that is currently selected within an ngFor loop:

Currently, I am using an ngFor loop to display multiple membership options. My goal is to modify the text inside each button from 'select' to 'selected' once a user has made their choice. However, the issue I'm facing is that when ...

What is the right way to send a success response from Express JS to Backbone when logging out a user?

I'm currently learning how to work with Express JS and Backbone. On the server side using Express.js, I have this code snippet for logging out a user: app.get('/logout', function(req, res) { req.logout(); res.send('How can I ...

Is it possible to establish a CSS minimum width that is relative to a different element?

Is it possible to set the min-width of a submenu in CSS to be equal to that of the corresponding link in a navigation menu? I am using LESS for my styling. <ul> <li> <a href="">Item FooBarBaz</a> <ul class="submenu"&g ...

Props in Vue components are exclusively accessible within the $vnode

Exploring the realm of Vue.js, I am tasked with constructing a recursive Component renderer that transforms JSON into rendered Vue components. The recursive rendering is functioning smoothly; however, the props passed to the createElement function (code b ...

How can one pass a generic tuple as an argument and then return a generic that holds the specific types within the tuple?

With typescript 4 now released, I was hoping things would be easier but I still haven't figured out how to achieve this. My goal is to create a function that accepts a tuple containing a specific Generic and returns a Generic containing the values. i ...

Component state does not reset upon toggling

I have been exploring ways to conditionally display components. Handled externally: {show && <MyComponent />} Handled internally: const MyComponent = () => { const [externalState] = useContext(); const [state, setState] = useState(" ...

Getting the autogenerated document ID from Firestore - A Step-by-Step Guide

When creating my document, I rely on the auto-generated ID. However, after setting it, I find that the retrieved ID is different from the one in the Firestore Database. await admin.firestore().collection("mycollection").doc().set(mydata) .then(doc = ...

Error in JavaScript Slideshow

I am attempting to create a slider that changes with a button click. Below is my JavaScript code: var img1=document.getElementById("p1"); var img2=document.getElementById("p2"); var img3=document.getElementById("p3"); function slide(){ ...

Inserting an HTML element into Handlebars.js during a specific iteration of an each loop

I have a channel.json file containing 7 objects of data which are iterated in hb. I am looking for a way to insert a date between these iterations without modifying the .json file. How can I add an html tag to display after the 3rd iteration within the loo ...

Troubleshooting the issue with the sequelize hasOne association in Node.js

In my Node.js application utilizing Sequelize to access the database, I have tables named Products, Users, Carts, and CartItems. There are relationships between these tables: Product.belongsTo(User, { constraints: true, onDelete: 'CASCADE' }); ...

Angular Universal: Missing Title and Meta tags in Page Source

I need help with updating title and meta tags in my Angular Universal Application after a successful api call. app.component.ts import {Component, Inject, OnInit, PLATFORM_ID} from '@angular/core'; import {Meta, Title} from '@angular/platfo ...

React JS for loop not displaying any output

I am trying to create a component that loops from 0 to the value entered as a prop. if (props.number !== "" && props.toPow !== "") { for (let i = 0; i < props.toPow; i++) { return ( <div> & ...

Attempting to prompt a second activity by clicking a button

My current project involves developing an android app that includes launching a second activity from a button click in the main activity. However, I'm encountering the following error: Error Cannot Find Symbol Method startActivity(Intent) I've h ...

Having trouble setting a default value within an Angular component using ControlValueAccessor?

Demo: https://plnkr.co/edit/cMu3lI3PkxHRErJE3T93?p=preview I've encountered an issue with setting default values for ngModel or formControlName when using ControlValueAccessor in multiple components. For instance, in the provided demo, there is a se ...

Conceal the second click action within the anchor tag

Is there a way to hide the second click event on all anchor tags except those that trigger popupfun? I have a sample page set up. [Check out the JS Fiddle here][1] http://jsfiddle.net/ananth3087/LgLnpvf4/15/ Link Anchor Tags: The page includes two ...

Is it possible to transfer state to the getServerSideProps function in any way?

As a newcomer to next.js, I have a question about passing page state to getServerSideProps - is it achievable? const Discover = (props) => { const [page, setPage] = useState(1); const [discoverResults, setDiscoverResults] = useState(props.data. ...

"Dilemma: Why won't the AJAX form post load inside the

Apologies for the simple question, but I'm experiencing an issue where nothing happens when the button is pressed. Any assistance would be greatly appreciated. AJAX $(document).on("ready", function(){ //Form action $("#wbForm").on("submit", function( ...