Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce assets generation #3435

Merged
merged 10 commits into from Mar 21, 2018
Merged

Conversation

ivanovit
Copy link
Contributor

@ivanovit ivanovit commented Mar 9, 2018

Introduce assets generate icons and assets generate splashes commands to generate assets needed for iOS and Android.

The operations are as follow:

  • Icons:
    • For all platforms, we just resize the provided image.
  • Splash screens:
    • For Android we generate 2 png for each dpi: blank with the background(background.png) and resized image based on the provided image(logo.png). During the app start, the logo overlays the background.
    • For iOS we generate 1 png for each dpi which has resized and centered image on the background.

All resources with corresponding sizes and operations are described in image-definitions.ts.

Added image-generation-test.png in order to provide an easy way to test this functionality.

For image resizing we use https://github.com/oliver-moran/jimp.

lib/bootstrap.ts Outdated
@@ -144,3 +144,7 @@ $injector.requirePublic("extensibilityService", "./services/extensibility-servic

$injector.require("nodeModulesDependenciesBuilder", "./tools/node-modules/node-modules-dependencies-builder");
$injector.require("subscriptionService", "./services/subscription-service");

$injector.requireCommand("generate-icons", "./commands/generate-assets")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've discussed this internally, and we would like to introduce several commands related to the resources of the project. So can you please rename the commands to resources|generate|icons and resources|generate|splashscreens.
We may also introduce shorthands for these commands, for example res|gen|icons, res|gen|splashes, but lets discuss this further.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've agreed the commands to be:

resources|generate|icons
resources|generate|splashes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usage will be:

tns resources generate icons path_to_image 
tns resources generate splashes path_to_image --background <color> 

lib/bootstrap.ts Outdated

$injector.requireCommand("generate-icons", "./commands/generate-assets")
$injector.requireCommand("generate-splashscreens", "./commands/generate-assets")
$injector.requirePublic("assetsGenerationService", "./services/assets-generation/assets-generation-service");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new line at the end of file please

package.json Outdated
@@ -30,13 +30,15 @@
"mobile"
],
"dependencies": {
"@types/color": "^3.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be in the devDependencies section. Also, can you please use exact version.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

npm i --save-dev --save-exact @types/color

package.json Outdated
"bplist-parser": "0.1.0",
"bufferpack": "0.0.6",
"byline": "4.2.1",
"chalk": "1.1.0",
"chokidar": "1.7.0",
"cli-table": "https://github.com/telerik/cli-table/tarball/v0.3.1.2",
"clui": "0.3.1",
"color": "^3.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strict version, please (you can use npm i --save --save-exact color in order to update package.json and npm-shrinkwrap.json correctly)

package.json Outdated
@@ -48,6 +50,7 @@
"ios-device-lib": "0.4.10",
"ios-mobileprovision-finder": "1.0.10",
"ios-sim-portable": "3.3.0",
"jimp": "^0.2.28",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, strict version please

}

@exported("assetsGenerationService")
public async generateIcons(imagePath: string, resourcesPath: string): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use object as single argument (you'll have to introduce interface for it). This will allow us to handle future changes easier

}

@exported("assetsGenerationService")
public async generateSplashScreens(imagePath: string, resourcesPath: string, background?: string): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use object as single argument (you'll have to introduce interface for it). This will allow us to handle future changes easier


private generateImage(background: string, width: number, height: number, outputPath: string, overlayImage?: Jimp) {
//Typescript declarations for Jimp are not updated to define the constructor with backgroundColor so we workaround it by casting it to <any> for this case only.
let J = <any>Jimp;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be const

}
}

private async resize(imagePath: string, width: number, height: number) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method needs return type

return image.scaleToFit(width, height);
}

private generateImage(background: string, width: number, height: number, outputPath: string, overlayImage?: Jimp) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method needs return type. Also can you use a single object for parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not part of the public interface and I think it will be better to leave it as it is.

@rosen-vladimirov rosen-vladimirov added this to the 4.0.0 milestone Mar 12, 2018
@petekanev
Copy link
Contributor

@ivanovit @rosen-vladimirov
The paths in https://github.com/NativeScript/nativescript-cli/pull/3435/files#diff-ec7f1f51cae0151d8566c9bce4534772 to the android resources directory will only work in case the internal directory structure has not been changed. If it's been updated - as a result of running tns resources update beforehand, then the resources need to go to Android/src/main/res/

@ivanovit ivanovit force-pushed the IIIvanov/Assets-Generation branch 2 times, most recently from 18398b8 to 6f03ed7 Compare March 14, 2018 13:38
@ivanovit
Copy link
Contributor Author

ping @rosen-vladimirov @Pip3r4o

Copy link
Contributor

@rosen-vladimirov rosen-vladimirov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! 🎉 💯 🥇

Copy link
Contributor

@petekanev petekanev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for addressing that comment!

@ivanovit ivanovit changed the base branch from master to release March 15, 2018 09:53
@exported("assetsGenerationService")
public async generateIcons(resourceGenerationData: IResourceGenerationData): Promise<void> {
this.$logger.info("Generating icons ...");
await this.generateImagesForDefinitions(resourceGenerationData.imagePath, resourceGenerationData.resourcesPath, Icons, resourceGenerationData.platform);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the platform argument should be fifth

@@ -12,21 +12,23 @@ export class AssetsGenerationService implements IAssetsGenerationService {
@exported("assetsGenerationService")
public async generateIcons(resourceGenerationData: IResourceGenerationData): Promise<void> {
this.$logger.info("Generating icons ...");
await this.generateImagesForDefinitions(resourceGenerationData.imagePath, resourceGenerationData.resourcesPath, Icons);
await this.generateImagesForDefinitions(resourceGenerationData.imagePath, resourceGenerationData.resourcesPath, Icons, resourceGenerationData.platform);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we introduce an interface and pass object as param?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it is a private method and I have some concerns about polluting the .d.fs only for this method I will leave it as it is.

super($options, $injector, $projectData, $stringParameterBuilder, $assetsGenerationService);
}

protected async generate(imagePath: string, background?: string): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second argument here is unnecessary and could be omitted (I think it would still conform to the signature of the Base command)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to keep it just to ensure the same signature

super($options, $injector, $projectData, $stringParameterBuilder, $assetsGenerationService);
}

protected async generate(imagePath: string, resourcesPath: string, background?: string): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signature of the base method differs from this one and the third argument will never be passed - I think we'd need to delete resourcesPath: string as a whole?

@@ -463,6 +463,7 @@ interface IOptions extends ICommonOptions, IBundleString, IPlatformTemplate, IEm
liveEdit: boolean;
chrome: boolean;
inspector: boolean; // the counterpart to --chrome
background: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing ;

@@ -0,0 +1,122 @@
import * as Jimp from "jimp";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the pascal case - this is actually an instance object and should be camelCase

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is used as a constructor, so it makes sense to be with pascal case

this.$logger.info("Splash screens generation completed.");
}

private async generateImagesForDefinitions(data: IGenerateImagesData): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could accept data: IResourceGenerationData, propertiesToEnumerate: string[] here in order to avoid casting to IGenerateImagesData in the caller methods above.
This is purely cosmetic and not a merge-stopper at all

const assetsStructure = await this.$projectDataService.getAssetsStructure({ projectDir: data.projectDir });

for (const platform in assetsStructure) {
if (data.platform && platform.toLowerCase() !== data.platform.toLowerCase()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this whole logic you can construct the required array (an array of values gotten from assetsStructure) using the lodash filter.
You could filter out all the unneeded platforms and items that do not include imageTypeKey.
You would then be able to skip all the continue statements that simulate filtering


const imageDefinitions = this.getImageDefinitions().ios;

if (content && content.images) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can skip this check since you're using the lodash each method - it could just be:
_.each(content && content.images, image => {

_.each(assetItems, assetItem => {
_.each(normalizedPaths, currentNormalizedPath => {
const imagePath = path.join(assetItem.directory, assetItem.filename);
if (currentNormalizedPath.indexOf(path.normalize(imagePath)) !== -1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use _.find to locate the value of interest instead of using _.each and then return false (e.g. break)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've used find at the beginning, but it looks strange, so I prefer the each

};
}

private getImageDefinitions(): any {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this method returns IAssetsStructure

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately it does not :(

}
} else {
// Find the image size based on the hardcoded values in the image-definitions.json
_.each(imageDefinitions, (assetSubGroup: IAssetItem[]) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't need to point the type of assetSubGroup out if imageDefinitions was strongly typed - I believe its type is actually IAssetsStructure

ivanovit and others added 9 commits March 21, 2018 10:09
Introduce ``generate-icons`` and ``generate-splashscreens`` commands to generate assets needed for iOS and Android.
The operations are as follow:
 - Icons:
  - For all platforms we just resize the provided image.
 - Splash screens:
  - For Android we generate 2 png for each dpi: blank with background(background.png) and resized image based on provided image(logo.png). During the app start the logo overlays the background.
  - For iOS we generate 1 png for each dpi which has resized and centered image on the background.

All resources with corresponding sizes and operations are described in image-definitions.ts.

Added image-generation-test.png in order to provide easy way to test this functionality.

For image resizing we use https://github.com/oliver-moran/jimp.
Introduce new methods in `projectDataService` to get the current assets strucuture. Use a predefined .json file in the resources to get information about image sizes.
For iOS read the Contents.json file in specific iOS Resource directories.
For Android - enumerate the files and construct required objects.

Use the new method in the assetsGenerationService. Generate icons and splashes based on the argument.
Require projectDir instead of resourcesDir for assets generation in order to get all CLI's specific files info (nsconfig, package.json, project location, etc.).

Fix comands for generating assets to have mandatory command parameter - previously it had a non-mandatory param.
Add newly exposed methods to the tests.
@rosen-vladimirov rosen-vladimirov merged commit d341285 into release Mar 21, 2018
@rosen-vladimirov rosen-vladimirov deleted the IIIvanov/Assets-Generation branch March 21, 2018 12:02

for (const assetItem of assetItems) {
const operation = assetItem.resizeOperation || Operations.Resize;
const scale = assetItem.scale || 0.8;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scale option must be respected for the Resize operation as well. The scale must be parsed to string because the value there is 1x, 2x.

.value();

for (const assetItem of assetItems) {
const operation = assetItem.resizeOperation || Operations.Resize;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you infer the image operation for the asset only from Content.json?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is taken in project-data-service for Android, but I've missed to set it for iOS 😿

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants