React Native provides a unified way of managing images in iOS and Android applications. Static resource images can be included in a React Native projects as explained in Static Image Resources by using a require statement. As an example, a component called MyCreditCardComponent
that needs to display credit card images can load a static image from within the project with a JSX tag like this:
class MyCreditCardComponent extends React.Component
{
render()
{
return <Image source={ require( '../images/Icon_AmericanExpress.png' ) } />
}
}
The appropriate directory structure for this scenario would include the original image in native resolution, as well as 2x
and 3x
renderings of the same image.
app
├── components
│ └── MyCreditCardComponent.js
└── images
├── Icon_AmericanExpress.png
├── Icon_AmericanExpress@2x.png
└── Icon_AmericanExpress@3x.png
The 2x
and 3x
renderings of the image are used for higher resolution displays, for instance the Apple Retina display.
It is often the case in a project that the artwork is created in a vector format. Raster PNG files are then manually created in all required resolutions. Doing it can be a tedious process and change control is difficult. Also, the original vector images are often not part of the same project and are not in the same source control repository. They are not kept up to date with the respective commits, merges, etc.
Using SVG files to save the original vector artwork, storing them within the project and using a script to convert them to raster PNG images in the appropriate resolutions resolves that problem.
The tool build-app-images
within Rebar's tools package automates this process by having the source vector file, stored as SVG, to be automatically rendered into the three PNG images required. The directory structure for this scenario is:
app
├── components
│ └── MyCreditCardComponent.js
└── images
├── Icon_AmericanExpress.svg
├── Icon_AmericanExpress.png
├── Icon_AmericanExpress@2x.png
└── Icon_AmericanExpress@3x.png
Notice that the SVG file will not be included in the React Native bundle, since only require
-ed PNG files are included.
In the Rebar/Universal Relay Boilerplate directory structure all app related custom components are parts of units which are structures in an .\units
directory. The app
directory, as shown above, would be part of such an unit, in this case names rb-payments
:
./
└── units
├── unit1
│ .....
├── rb-payments
│ ├── components
│ │ └── MyCreditCardComponent.js
│ └── images
│ ├── Icon_AmericanExpress.svg
│ ├── Icon_AmericanExpress.png
│ ├── Icon_AmericanExpress@2x.png
│ └── Icon_AmericanExpress@3x.png
│ .....
└── unitX
By convention all SVG files that need to be converted are stored in the app/images
sub-directory of the unit directory. The function below simply goes through all possible locations and finds all SVG files. Then it converts them one by one by calling convertSingleSVG2PNG
.
function searchAndConvertSVG2PNG( directoryName: String ): void
{
fs.readdirSync( directoryName )
.filter( unitName =>
{
if( fs.statSync( directoryName + unitName )
.isDirectory() )
{
const imagesDir = path.resolve( directoryName, unitName, 'app/images' )
fs.readdirSync( imagesDir )
.filter( pngName =>
{
if( pngName.endsWith( '.svg' ) )
{
convertSingleSVG2PNG( imagesDir, pngName )
}
} )
}
} )
}
The script is started with the following function call:
searchAndConvertSVG2PNG( 'units/' )
Each SVG image file is converted/rendered into three sizes required by the x1 x2 and x3 resolutions. The conversion function is run three times:
function convertSingleSVG2PNG( directoryName: String, svgName: String ): void
{
const imageName = svgName.substring( 0, svgName.length - 4 )
const svgData = {}
convertSingleSVG2PNGWithAmplification( svgData, directoryName, imageName, 1 )
convertSingleSVG2PNGWithAmplification( svgData, directoryName, imageName, 2 )
convertSingleSVG2PNGWithAmplification( svgData, directoryName, imageName, 3 )
}
The actual conversion function for a certain size has the following structure:
function convertSingleSVG2PNGWithAmplification( svgData: any, directoryName: String, imageName: String, amplification: Number ): void
{
try
{
// Load the SVG file, determine the width, height and last modified date into a structure:
// {
// buffer: Node.js buffer
// width: Number, must be in pixels
// height: Number, must be in pixels
// lastModified: Date
//
// }
// Determine PNG file name
// Determine last modified and compare to SVG. Use 1/1/1970 if not found so that SVG appears older
// Generate PNG only if SVG is newer
}
catch( err )
{
throw new Error( "Could not convert " + directoryName + "." + imageName + ".svg because: " + err.message )
}
}
Notice that the SVG data will be loaded only once, since the same svgData
object is passed in every call to the function for the same SVG.
The data about the SVG file is retrieved in a straightforward piece of code:
if( !( svgData.buffer ) )
{
// Determine the SVG file name
const svgFileName = path.resolve( directoryName, imageName + '.svg' )
// Read file
const svgBuffer = fs.readFileSync( svgFileName )
// Read as XML and get width and height
const svgXml = xml.parseBuffer( svgBuffer )
const width = parseDimension( svgXml.attrib.width, "Width", directoryName, imageName )
const height = parseDimension( svgXml.attrib.height, "Height", directoryName, imageName )
// Get last modified date/time
const lastModified = getLastModifiedDateTimeForFile( svgFileName )
Object.assign( svgData,
{
buffer: svgBuffer,
width,
height,
lastModified
} )
console.log( "Processing SVG: " + svgFileName + ", last modified: " + renderPrettyDate( lastModified ) )
}
The Node.js fs
module is used for the file reading and also getting the last modified date and time:
function getLastModifiedDateTimeForFile( fileName: String ): Date
{
const stats = fs.statSync( fileName )
return new Date( util.inspect( stats.mtime ) )
}
For the XML parsing, the excellent node-xml-lite
* module is used. The SVG must have declared width and height in pixels. The parsing of the dimensions is done in the following fashion:
function parseDimension( dimensionValue: String, dimensionName: String, directoryName: String, imageName: String ): Number
{
const dimensionAsString = dimensionValue
if( !dimensionAsString.endsWith( "px" ) )
throw new Error( dimensionName + " must end with px, but it rather is: " + dimensionAsString )
const dimensionAsInt = parseInt( dimensionAsString )
if( dimensionAsInt == NaN )
throw new Error( dimensionName + " must be integer, but it rather is: " + dimensionAsString )
return dimensionAsInt
}
Here is an example of a valid SVG image of a credit card taken from Material UI Credit Card Icons:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px"
width="60px"
viewBox="0 0 60 40" enable-background="new 0 0 60 40" xml:space="preserve">
<g>
<!-- content of the svg file -->
</g>
</svg>
Notice the presence of width="60px"
and height="40px"
attributes.
Straightforward code retrieves the last modified date, or if the file is missing, uses 1/1/1970
:
// Determine PNG file name
const amplificationString = amplification > 1 ? '@' + amplification + 'x' : ''
const pngFileName = path.resolve( directoryName, imageName + amplificationString + '.png' )
// Determine last modified and compare to SVG. Use 1/1/1970 if not found so that SVG appears older
let pngLastModified = null
try
{
pngLastModified = getLastModifiedDateTimeForFile( pngFileName )
}
catch( err )
{
if( err.code === 'ENOENT' )
pngLastModified = new Date( 0 )
else throw err
}
Rendering the SVG file into PNG is performed using the excellent excellent svg2png
* module. The rendering takes significantly more time than scanning the directories for SVG files and loading the file information. In order to speed up the execution, a PNG file is rendered only if:
1/1/1970
above;Here is the code:
if( pngLastModified <= svgData.lastModified )
{
const width = amplification * svgData.width
const height = amplification * svgData.height
console.log( "Rendering PNG: " + pngFileName + ", width: " + width + ", height: " + height )
const pngBuffer = svg2png.sync( svgData.buffer,
{
width,
height
} )
fs.writeFileSync( pngFileName, pngBuffer )
}
else
console.log( "Skipping PNG: " + pngFileName + ", last modified: " + renderPrettyDate( pngLastModified ) )
The tool can be used within Rebar by running:
yarn build-app-images
For examples of using the script please read the description in the rb-tools
unit's documentation.