Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Handling Images with the CopyPlugin

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Bonus! A really cool side-effect of using Webpack is that none of these files in the assets/ directory need to be public anymore! I mean they live in the public directory currently... but the user never needs to access them directly: Webpack processes and moves them into build/.

To celebrate, let's move assets/ out of the public/ directory and into the root of our project. We don't need to do this... but if something doesn't need to be publicly accessible, why make it public?

This change breaks almost nothing. The only things we need to update are the paths in webpack.config.js:

... lines 1 - 2
Encore
... lines 4 - 9
.addEntry('rep_log', './assets/js/rep_log.js')
.addEntry('login', './assets/js/login.js')
.addEntry('layout', './assets/js/layout.js')
... lines 13 - 16
;
... lines 18 - 21

After making that change, restart Encore!

yarn run encore dev --watch

And... refresh! Woohoo! Wait... there's a missing image! Bah! I was lying! There is one file that still needs to be publicly accessible!

Open index.html.twig... ah! We have a good, old-fashioned img tag that references one of the images in the assets/ directory:

... lines 1 - 2
{% block body %}
<div class="row">
... lines 5 - 34
<div class="col-md-5">
<div class="leaderboard">
<h2 class="text-center">
<img class="dumbbell" src="{{ asset('assets/images/dumbbell.png') }}" />
... line 39
</h2>
... lines 41 - 42
</div>
</div>
</div>
{% endblock %}
... lines 47 - 59

And... whoops! It's not public anymore. My bad!

This is one of the few cases - maybe the only case - where we need to reference public images from outside a file that Webpack processes. The simple problem is that Webpack doesn't know that it needs to move this file!

Of course, there's an easy fix: we could just move this one file back into the public/ directory. But... that sucks: I'd rather keep all of my assets in one place.

Installing copy-webpack-plugin

Tip

Great news! The latest version of Encore has a copyFiles()! You can use this instead of installing this plugin.

To do this, we can take advantage of a Webpack plugin that can copy the file for us. Google for copy-webpack-plugin to find its GitHub page. Encore gives you a lot of features... but it doesn't give you everything. But... no worries! We're using Webpack under-the-hood. So if you find a Webpack plugin you want, you can totally use it!

Side note, Encore will have a copy() method soon. Then you'll be able to do this without a plugin. Yay! But, this is still a great example of how to extend Webpack beyond Encore.

Anyways, install the plugin first. Notice that they use npm. I'm going to use yarn. So copy the name of that plugin, find your terminal, and run:

yarn add copy-webpack-plugin --dev

Adding Custom Webpack Config

To use the plugin, we need to require it at the top of the Webpack config file. No problem:

var Encore = require('@symfony/webpack-encore');
const CopyWebpackPlugin = require('copy-webpack-plugin');
... lines 3 - 27

And then below, um.... config =... and plugins:... what the heck does this mean?

Well... earlier, I told you that webpack.config.js normally returns a big configuration object. And Encore is just a tool to help generate that config. In fact, at the bottom, we can see what that config looks like if we want! Just console.log(module.exports).

Then, restart Encore:

yarn run encore dev --watch

Woh! There's our config! Actually, it's not so scary: there are keys for entry, output, module, plugins and a few other things.

For example, see the plugins key? Back on their docs, that is what they're referring to: they want you to add their plugin to that config key.

Ok, so how can we do that? Well, you could always just add it manually: module.exports.plugins.push() and then the plugin. Yep: you could literally add something to the plugins array!

But, fortunately, Encore gives you an easier way to modify the most common things. In this case, use addPlugin() and then new CopyWebpackPlugin(). Pass this an array - this will soon be the paths it should copy:

... line 1
const CopyWebpackPlugin = require('copy-webpack-plugin');
Encore
... lines 5 - 18
.addPlugin(new CopyWebpackPlugin([
... lines 20 - 21
]))
;
... lines 24 - 27

Copying Images into build/

But, before we fill that in... let's think about this. I don't need to copy all of my images to the build/ directory... just one of them right now. So let's create a new directory called static/ and move any files that need to be copied into that directory, like dumbell.png.

In the CopyWebpackPlugin config, set from to ./assets/static and to to just static:

... line 1
const CopyWebpackPlugin = require('copy-webpack-plugin');
Encore
... lines 5 - 18
.addPlugin(new CopyWebpackPlugin([
// copies to {output}/static
{ from: './assets/static', to: 'static' }
]))
;
... lines 24 - 27

This will copy to the output directory /static.

Ok, go restart Encore!

yarn run encore dev --watch

Once the build finishes... inside public/build... yes! We have a new static directory. It's nothing fancy, but this is a nice way to move files so that we can reference them publicly in a template:

... lines 1 - 2
{% block body %}
<div class="row">
... lines 5 - 34
<div class="col-md-5">
<div class="leaderboard">
<h2 class="text-center">
<img class="dumbbell" src="{{ asset('build/static/dumbbell.png') }}" />
... line 39
</h2>
... lines 41 - 42
</div>
</div>
</div>
{% endblock %}
... lines 47 - 59

There's one more reference in the login template: search for "bell" and... update this one too:

... lines 1 - 16
{% block fos_user_content %}
<div class="container">
<div class="wrapper">
<form action="{{ path("fos_user_security_check") }}" method="post" class="form-signin">
<h3><img class="dumbbell" src="{{ asset('build/static/dumbbell.png') }}">Login! Start Lifting!</h3>
... lines 22 - 67
</form>
</div>
</div>
{% endblock fos_user_content %}

Try it! Find your browser and refresh. There it is!

Next, let's make our CSS sassier... with... Sass of course!

Leave a comment!

24
Login or Register to join the conversation

I didn't understand the difference between using this plugin and copyFiles () Encore native function?

Reply

Hey ahmedbhs!

Sorry for my SUPER slow reply to this great question! This tutorial was created before that copyFiles() feature was added. And, though they both "copy things", the implementation is completely different. The issue with this plugin - which we could never get them to improve, and hence, we we added an entirely different implementation in copyFiles(), is that the copied files wouldn't show up in your manifest.json file. This mean that you couldn't take advantage of versioned filenames (like you do with CSS and JS).

So, that's the difference: they do the same thing, work completely different under the hood, and copyFiles() supports versioning :).

Cheers!

Reply
Csaba B. Avatar
Csaba B. Avatar Csaba B. | posted 4 years ago

I'm stuck with css file background image urls..

a) If I set: background-image: url('/asssets/images/bg/03.jpg'); the yarn encore dev command run successful, then I see the copied image in the public/build directory. Thats ok.
But when the browser try to access '/asssets/images/bg/03.jpg' file drop 404 (of course) because css file url need to change and point to the public dir.

b) If is set background-image: url('images/bg/03.jpg'); (in the public dir) the yarn encore dev command failed:
"ERROR Failed to compile with 1 errors. This relative module was not found"

How can I define background image url in assets/css/app.css ?

Reply
Csaba B. Avatar

Okay, I solved it:
If I set background-image url to: url('/build/images/bg/03.jpg'); in the app.css the css file buliding will be success.

Reply

Awesome! I'm glad that you could fix your problem and share the solution :)

Cheers!

Reply
Default user avatar
Default user avatar Awd Studio | posted 4 years ago

Hi! Tnx for this tuto, it is really helpful.
Maybe you know some way to use it with HMR?
(I mean `encore dev-server --hot`)

Reply

Hey Awd Studio

I'm not sure I'm following you. Do you want to know how to activate Encore's dev server?

Reply
Default user avatar
Default user avatar Xavier de Moor | posted 5 years ago

So, copying images or files will never work for what I'm seeing? Almost a year this feature is awaiting in the copy plugin... I was thinking webpack encore (or Laravel mix) can replace Gulp, but I think I will need to use webpack AND Gulp or just continue to use Gulp just to have a correct manifest? :/

Reply
Default user avatar

I'm a little bit disappointed. I read all this tutorial since this step for nothing :( I know it's not your fault.

Reply

Hey Xavier de Moor!

Yea, this whole copying thing WITH versioning (copying works fine without versioning) has been a pain for a long time :/. It's something I hope to fix - we were waiting on a change in the copy plugin... then it still wasn't enough - we may just add our own copy functionality.

But, I did recently learn about a trick - I haven't tried it yet, but check this out: https://twitter.com/SBISDan...

This will copy all of the assets and put them into the manifest. It's an awesome little workaround - we'll document it soon.

Cheers!

Reply
Default user avatar
Default user avatar Xavier de Moor | weaverryan | posted 5 years ago

I think adding your own copy with versioning is a very good idea. I will stay in touch. Thank you for your answer!

Reply
Default user avatar
Default user avatar Quentin | posted 5 years ago

Hey, thank you for this cool tutorial! We implemented it but we have one problem, in production our manifest.json is not taking into account images copied with CopyPlugin, do you know a way to fix this or to work around of this problem?

Reply

Hey Quentin!

Yes, unfortunately, you're 100% correct. In fact, this is the reason that we haven't added the copy() method to Encore yet. We needed to wait for the webpack-manifest-plugin to tag their version 2.0, which contains a "hook" for allowing custom data to be added. We're *still* waiting for that release :/. Here is a related issue: https://github.com/webpack-...

So for now, it's either not possible, or you will need to follow issues like the one linked to get it working. For this reason, I do *not* use a `hash` when I use the copy plugin. Yes, this means that any copied assets aren't versioned - that's the big bummer currently.

Hopefully the webpack-manifest-plugin will get its 2.0 tag soon, and we can try to fit all the pieces together!

Cheers!

1 Reply
Default user avatar
Default user avatar RoestVrijStaal | weaverryan | posted 5 years ago

Thanks for wasting ~2 hours of my life for wasted efforts to get the stuff working for my project's images.

Could you please put a big banner at the top of this tuto with the text "NOT FOR IMAGES YET" for the time
being (read: forever, since the pull request is still open for months without any change) so others will look for a different solution than copy-webpack-plugin?

Why are you publishing this tutorial anyway, when copy-webpack-plugin doesn't fullfill its intended goal?

Reply

Hey RoestVrijStaal!

Oh man, I'm sorry that you wasted your time - that's definitely not our intention :/. This one is tough, we (from Webpack Encore) have been waiting for months for the upstream library to add some features we need - and we thought it would happen quickly. Now it appears that it won't, and we need to find a workaround.

Why did we post this tutorial then? Because, people *do* need to be able to move their images into their build directory. And if you use the plugin like we do in this tutorial - *without* a [hash] in the filename - then it works perfectly fine :). You only have problems if you try to use [hash], because then your backend code doesn't know what dynamic filename to use. That's the problem we hope to solve some time.

Anyways, I'm sorry you were frustrated - that is the opposite of our intention! I hope you'll enjoy other parts of Webpack Encore!

Cheers!

Reply
Default user avatar
Default user avatar Quentin | weaverryan | posted 5 years ago | edited

Hey weaverryan, thank you for you response. I subscribed to this issue and I switched to a previous version of my code waiting webpack-manifest-plugin 2.0.

Regards

Reply
Default user avatar

Thanks for this free tutorial guys. One little question, can we use webpack encore to optimise images? I was wondering this because with Assetic it's possible right?

Reply

Hey Cesar!

Glad you're liking the tutorial :). The answer your question is no... at least currently: there's no feature in Encore today to optimize images. However, you can add any Webpack plugins or loaders to Encore that you want. For example, check out https://github.com/tcoopman/image-webpack-loader - I've never used it before, just googled for it quickly. You could add this to Encore with those code:


Encore
    // ...
    .addLoader({
        test: /\.(gif|png|jpe?g|svg)$/i,
        use: [
            'file-loader',
            {
                loader: 'image-webpack-loader',
                options: {
                    bypassOnDebug: true,
                },
            }
        ]
}

So, if that's something you're interested in, try it out!

Cheers!

Reply

Hello. Could you help me to find the error and tell me why I can't optimise the images
Code:



// webpack.config.js

var Encore = require('@symfony/webpack-encore');

const CopyWebpackPlugin = require('copy-webpack-plugin');

const ImageOptimizePlugin = require('image-webpack-loader');

Encore

// directory where compiled assets will be stored

    .setOutputPath('public/build/')

    .setPublicPath('/build')

    .createSharedEntry('style', './assets/js/style.js')

    .addPlugin(new CopyWebpackPlugin([

        // copies to {output}/static

        {from: './assets/static', to: 'static'}

    ]))

    .addLoader({

        test: /\.(gif|png|jpe?g|svg)$/i,

        use: [

            'file-loader',

            {

                loader: 'image-webpack-loader',

                options: {

                   bypassOnDebug: true,

                    mozjpeg: {

                        progressive: true,

                        quality: 65

                    },

                    optipng: {

                        enabled: true,

                    },

                    pngquant: {

                        quality: '65-90',

                        speed: 4

                    },

                    gifsicle: {

                        interlaced: true,

                    },

                    webp: {

                        quality: 75

                    }

                },

            }

        ]

    })

    .enableBuildNotifications()

    .autoProvidejQuery()

    .enableSassLoader()

    .cleanupOutputBeforeBuild()

    .enableVersioning()

    .disableSingleRuntimeChunk()

;

module.exports = Encore.getWebpackConfig();

After web pack run:


Running webpack ...

 DONE  Compiled successfully in 694ms                                                                                                                                        14:17:53

 I  3 files written to public/build

Entrypoint _tmp_shared = style.dd43f399.css style.922f1cea.js _tmp_shared.3b14f662.js

After I check the file size by ls -la public/build/static it have no changes.

Reply

Hi @Станислав Гидлевский!

I think I have an idea :). The image-webpack-loader will optimize any images that you "require" from your code - like this https://symfony.com/doc/current/frontend/encore/copy-files.html#referencing-images-from-inside-a-webpacked-javascript-file

But, the CopyWebpackPlugin simply copies the files - without having Webpack really process them. But, good news! The latest version of Encore (which it appears you're using), has a new copyFiles() function! If you use this, I'm pretty sure the image optimizer will work. 🎆

Cheers!

Reply

Thanks for the fast reply Ryan. I try to add these code ` .copyFiles({

        from: './assets/static',
        to: 'images/[path][name].[ext]',
    }
)` to my config file, but it seems ImageWebpackLoader doesn't trigger it. Just copy the files to the build folder as CopyWebpackPlugin. I added this code `const imagesCopy = require.context('../static', true, /\.(png|jpg|jpeg|gif|ico|svg|webp)$/);

imagesCopy.keys().forEach(imagesCopy);` to my js file and it workes, but I suppose this is not the best way.

Reply

Hey @Станислав Гидлевский!

Hmm, interesting! But, nice work around. Actually, this is basically what the copyFiles() method does internally... but it's possible that it's written in a way that makes this not possible. We'll need to check into this! But, your workaround is just fine - I don't see any issues with it.

Cheers!

Reply

Thanks. I've made some more progress. This code works fine without any additional scripts:


// webpack.config.js

var Encore = require('@symfony/webpack-encore');

const CopyWebpackPlugin = require('copy-webpack-plugin');
const ImageOptimizePlugin = require('image-webpack-loader').default;
const ImageminPlugin = require('imagemin-webpack-plugin').default;
const imageminMozjpeg = require('imagemin-mozjpeg');
const imageminOptipng = require('imagemin-optipng');
const imageminPngquant = require('imagemin-pngquant');

Encore

// directory where compiled assets will be stored

    .setOutputPath('public/build/')

    .setPublicPath('/build')

    .createSharedEntry('style', './assets/js/style.js')

    .addPlugin(new CopyWebpackPlugin([{

            from: './assets/static',

            to: 'static'

        }])

    )

    .addPlugin(new ImageminPlugin({

        test: /\.(jpe?g|png|gif|svg)$/i,

        minFileSize: 10000,

        maxConcurrency: Infinity,

        //disable: process.env.NODE_ENV !== 'production',

        pngquant: {

            quality: 55,

            speed: 3,

            verbose: true,

            strip: true,

        },

        plugins: [

            imageminMozjpeg({

                quality: 65,

                progressive: true

            }),

        ]

    }))

    .enableSourceMaps(!Encore.isProduction())

    .enableBuildNotifications()

    .autoProvidejQuery()

    .enableSassLoader()

    .cleanupOutputBeforeBuild()

    .enableVersioning()

    .disableSingleRuntimeChunk()

;

module.exports = Encore.getWebpackConfig();

So I use imagemin-webpack-plugin as Plugin instead of image-webpack-loader as Loader.

Reply
Default user avatar

Thanks for your answer Ryan.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^1.6", // 1.8.1
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.2
        "doctrine/doctrine-fixtures-bundle": "~3.0", // 3.0.2
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.3.1
        "doctrine/orm": "^2.5", // v2.7.2
        "friendsofsymfony/jsrouting-bundle": "^2.2", // 2.2.0
        "friendsofsymfony/user-bundle": "dev-master", // dev-master
        "sensio/framework-extra-bundle": "^5.1", // v5.1.5
        "symfony/asset": "^4.0", // v4.0.4
        "symfony/console": "^4.0", // v4.0.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.0.4
        "symfony/framework-bundle": "^4.0", // v4.0.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/monolog-bundle": "^3.1", // v3.1.2
        "symfony/polyfill-apcu": "^1.0", // v1.7.0
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/swiftmailer-bundle": "^3.1", // v3.1.6
        "symfony/twig-bundle": "^4.0", // v4.0.4
        "symfony/validator": "^4.0", // v4.0.4
        "symfony/yaml": "^4.0", // v4.0.4
        "twig/twig": "2.10.*" // v2.10.0
    },
    "require-dev": {
        "symfony/debug-pack": "^1.0", // v1.0.4
        "symfony/dotenv": "^4.0", // v4.0.4
        "symfony/phpunit-bridge": "^4.0", // v4.0.4
        "symfony/web-server-bundle": "^4.0" // v4.0.4
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@symfony/webpack-encore": "^0.19.0", // 0.19.0
        "bootstrap": "3", // 3.3.7
        "copy-webpack-plugin": "^4.4.1", // 4.4.1
        "font-awesome": "4", // 4.7.0
        "jquery": "^3.3.1", // 3.3.1
        "node-sass": "^4.7.2", // 4.7.2
        "sass-loader": "^6.0.6", // 6.0.6
        "sweetalert2": "^7.11.0", // 7.11.0
        "webpack-notifier": "^1.5.1" // 1.5.1
    }
}
userVoice