Crop image and send it to jsf bean without unnecessary data transfer - jsf

I'd like to upload an image generated by a jquery cropper to a bean field.
Client side I ve found this:
<p:fileUpload
id="imgInp"
mode="simple" allowTypes="/(\.|\/)(gif|jpe?g|png)$/"/>
<img id="blah" src="#" alt="your image" />
<p:imageCropper image="" />
<script>
var reader = new FileReader();
reader.onload = function (e) {
$('#blah').attr('src', e.target.result);
}
function readURL(input) {
if (input.files && input.files[0]) {
reader.readAsDataURL(input.files[0]);
}
}
$("#imgInp").change(function(){
readURL(this);
});
</script>
It displays the image without uploading it but I can't get it in the cropper. So I use a jquery cropper but then I'm not sure how to get it in the bean (without going through servlets, because it's not a one time use). In other words I need to send an img through ajax to the bean.
Otherwise I'd use primeface but it has to go through the wire which I'd like to avoid if possible. From the example I've seen the image is static content on the server. Do I really have to save the image on the server ? Can't I keep it as object and convert the UploadedFile to something the cropper will accept ?
Soomething like this:
<p:fileUpload
mode="advanced" allowTypes="/(\.|\/)(gif|jpe?g|png)$/"
fileUploadListener="#{bean.uploadPicListenner}"
update="cropper"/>
<h:panelGroup id="cropper" >
<p:imageCropper image="#{bean.img}"/>
</h:panelGroup>
public void uploadPicListenner(FileUploadEvent e) {
img = e.getFile();
RequestContext.getCurrentInstance().update("ptform:cropper");
}

It's actually quite simple once the process is understood despite some misleading answers that can be found online. I hope this is gonna help someone in the future.
The technique I used is to :
Pick image
Once image is picked, display it in a cropper without sending it through the wire.
Here there are a few option and I choosed to : When user moves the cropper rectangle around, the coordinates of the rectangle populate an hidden input field.
Send the coordinate to bean and crop it on server side.
I did it this way because the cropper jquery lib I wanted to use didn't transform the image to base 64 and just gave the coordinates of a rectangle. However if someone want to send the cropped image directly in the future I figured it would be really easy. Just like I did except you have to put the cropped image as string base 64 in a hidden input text (instead of rectangle coordinates - this is explained under-) and transform it back on the server side, that's all. (I don't know how efficient / secure that is however). At least that resolved my issue I had with primefaces, which was not wanting to send unnecessary data over the wire multiple times.
1. First let's display the image without sending it to the server.
At this point when the image is displayed if you check inside the src tag of the img you will see that it is the data of the image as base 64:
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAAQABAAD/2wCEAAYEBQYFBAYGBQ...
<h:form id="lolo" enctype="multipart/form-data">
<p:fileUpload
value="#{adminCreateTeam.teamImg}"
mode="simple" allowTypes="/(\.|\/)(gif|jpe?g|png)$/"/>
<img id="blah" src="#" alt="your image" />
</h:form>
<script>
var reader = new FileReader();
reader.onload = function (e) {
$('#blah').attr('src', e.target.result);
}
function readURL(input) {
if (input.files && input.files[0]) {
reader.readAsDataURL(input.files[0]);
}
}
$("#lolo\\:imgInp").change(function(){
readURL(this);
});
</script>
Once we have done that it becomes a bit dependent on the jquery cropping library used. I used the cropper lib. With that one we want to have the coordinate of the cropped rectangle. With the coordinates data we populate an hidden input and send it back to the bean to then recrop it on java side.
Alternatively a better solution (imo) would be to use a library that crop the image and give the data client side as base 64, populate an hidden field and send it back to the bean, to then convert base 64 to an image. Each of these steps is quiet easy and can be found on stackoverflow.
Since I wanted to use the cropper library I did it the first way:
This is added inside the form:
<h:inputHidden value="#{adminCreateTeam.rect}"/>
<p:commandButton value="submit" action="#{adminCreateTeam.picTest}" ajax="false"/>
This is the updated onload:
// with this the hidden field is gonna be populated by the
// cropping rectangle data.
var $imageCrop = $('#blah').cropper({
aspectRatio: 1/1,
viewMode: 1,
crop: function(e) {
// Output the result data for cropping image.
// string with all the data delimited by /
$('#lolo\\:hiddenB64').val(e.x + '/' + e.y + '/' + e.width + '/' + e.height);
}
});
//So the image changes in the cropper when a new image is picked
reader.onload = function (e) {
$imageCrop.cropper('replace',e.target.result);
}
We crop the image on with java
public void picTest() {
//getting coord.
String data[] = rect.split("/");
try (InputStream in = new ByteArrayInputStream(teamImg.getContents())) {
BufferedImage bImageFromConvert = ImageIO.read(in);
// line under this crops. It's possible there is a zoom to figure out, I didn't check yet. Seemed correct on first and only try. In any case you'll figure it out
// surely the parsing shouldn't be here but I need to sleep real bad.
BufferedImage dest = bImageFromConvert.getSubimage((int)(Double.parseDouble(data[0])), (int)(Double.parseDouble(data[1])),
(int)(Double.parseDouble(data[2])), (int)(Double.parseDouble(data[3])));
// path to the folder
Path folder = Paths.get(dirs.getString("imgTeams"));
String filename = "team_pic";
String extension = FilenameUtils.getExtension(teamImg.getFileName());
Path file = Files.createTempFile(folder, filename + "-", "." + extension);
ImageIO.write(dest, "jpeg", file.toFile());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

Related

svg convert to canvas - can't generate multi pages pdf

I have 12 graphs and I want to generate pdf with 2 pages each page has 6 graphs.
However, when I convert svg to canvas, then the jspdf can only see part of both sub-dives.
$('#downloadx2').click(function() {
var svgElements = $("#body_id").find('svg');
//replace all svgs with a temp canvas
svgElements.each(function() {
var canvas, xml;
// canvg doesn't cope very well with em font sizes so find the calculated size in pixels and replace it in the element.
$.each($(this).find('[style*=em]'), function(index, el) {
$(this).css('font-size', getStylex(el, 'font-size'));
});
canvas = document.createElement("canvas");
canvas.className = "screenShotTempCanvas";
//convert SVG into a XML string
xml = (new XMLSerializer()).serializeToString(this);
// Removing the name space as IE throws an error
xml = xml.replace(/xmlns=\"http:\/\/www\.w3\.org\/2000\/svg\"/, '');
//draw the SVG onto a canvas
canvg(canvas, xml);
$(canvas).insertAfter(this);
//hide the SVG element
////this.className = "tempHide";
$(this).attr('class', 'tempHide');
$(this).hide();
});
var doc = new jsPDF("p", "mm");
var width = doc.internal.pageSize.width;
var height = doc.internal.pageSize.height;
html2canvas($("#div_pdf1"), {
onrendered: function(canvas) {
var imgData = canvas.toDataURL(
'image/png', 0.1);
doc.addImage(imgData, 'PNG', 5, 0, width, height/2,'','FAST');
doc.addPage();
}
});
html2canvas($("#div_pdf2"), {
onrendered: function(canvas2) {
var imgData2 = canvas2.toDataURL(
'image/png', 0.1);
doc.addImage(imgData2, 'PNG', 5, 0, width, height/2,'','FAST');
doc.save('.pdf');
}
});
});
<body id="body_id">
<div id="div_pdf1" >
<svg></svg>
<svg></svg>
<svg></svg>
</div>
<div id="div_pdf1" >
<svg></svg>
<svg></svg>
<svg></svg>
</div>
</body>
When I run this code, the generated pdf will view two pages with same canvas the first one (div_pdf1) div. So how to get both of them appearing in pdf as two pages.
You seem to be trying to run 2 parts in sequence but that's not how javascript works and actually runs your code.
No big deal, just a small misunderstanding between your mental model and the engine that executes the code.
A quick temporary debugging tool to see what's going on and verify that there is a discrepancy is to add console.log to key points and check the sequence of their printout once you run the code.
console.log('[1] just before: svgElements.each');
svgElements.each(function() {
console.log('[2] just after: svgElements.each');
And also around this part of the code:
console.log('[3] just before html2canvas-div_pdf1');
html2canvas($("#div_pdf1"), {
console.log('[4] just after html2canvas-div_pdf1');
Finally around this part of the code:
console.log('[5] just before html2canvas-div_pdf2');
html2canvas($("#div_pdf2"), {
console.log('[6] just after html2canvas-div_pdf2');
I suspect you'll see the code doesn't print the log lines in the order you think they will.
Next, you can try wrapping the 2 calls to html2canvas with one setTimeout function and force a delay in the execution of that code by an arbitrary amount of milliseconds.
Note that this is not the recommended final production quality solution but it will make the code output what you want.

Show error and change doneLabel of rich:fileUpload in upload listener method

I have to upload CSV file and I have to validate some values of file, for example does not allow negative value. Then, I need validate it and that file does not apear like "done" or "uploaded". I need handle an error on display it.
<rich:fileUpload
fileUploadListener="#{configClientBean.listener}"
ontyperejected="alert('some error');"
maxFilesQuantity="1"
onuploadcomplete="#{rich:component('waitPanelInterno')}.hide();"
onfilesubmit="#{rich:component('waitPanelInterno')}.show();"
render="pnlMensajes, idTableClients, scrollRegistros, outputPanelDetalle"
autoclear="true">
<rich:message for="uploadConfigClient" />
<a4j:ajax event="uploadcomplete" execute="popupFileLoad"
render="panelCarga, pnlMensajes" />
</rich:fileUpload>
In the Backing bean I can validate some things, but I cant show errors or change the behavior of "rich:fileUpload" compoment for example doneLable can not be displayed.
public void listener(FileUploadEvent event) throws Exception {
try {
UploadedFile file = event.getUploadedFile();
ByteArrayInputStream bais = new ByteArrayInputStream(file.getData());
InputStreamReader is = new InputStreamReader(bais, getMessage("iso"));
BufferedReader bufRead = new BufferedReader(is);
while ((registro = bufRead.readLine()) != null) {
if(cvsLine[1].isEmpty()){
// stop process
// Throw error
}
}
}
Thanks for your time.
To add extra behavior to the rich:fileUpload component that is not default is by creating your own file upload component (ex. myown:fileUpload) based on the rich:fileUpload source code and the use of the Component Development Kit.
A second solution could be by adding one extra message field that is described in this post: RichFaces fileupload and h:message problem

uploading two files with JSF results in two times the same file

I have a JSF page + Bean to upload a file and preview the result (it's for images). In the bean the upload method puts the data (base64) in a list and the JSF page loops through the list to display the images.
So far it works as expected for the first image. But when I upload a second image there are two things that might go wrong:
the first image gets uploaded again instead
Or, with a little change in the code the second image does not get uploaded at all until I reselect the file again and upload it.
JSF page
<h:form enctype="multipart/form-data" id="uploadForm" prependId="false">
<!-- here I loop through the list to preview the images -->
<c:forEach var="image" items="#{bean.imageList}">
<h:graphicImage value="#{image}" />
</c:forEach>
<!-- that's the upload -->
<h:inputFile id="file" value="#{bean.uploadedFile}" />
<h:commandButton value="submit" action="#{bean.uploadFile}" />
</h:form>
bean
private Part uploadedFile; // the Part with the uploaded file
private ArrayList<String> imageList; // the list of images (base64 encoded)
public void uploadFile() throws IOException{
if(null != uploadedFile){
String imageType = "data:" + uploadedFile.getContentType() + ";base64,"; // will be something like: "data:image/png;base64,"
InputStream inputStream = uploadedFile.getInputStream(); // getting the inputStream from the Part
byte[] imageData; // array to but the image data
try(BufferedInputStream bis = new BufferedInputStream(inputStream)){ // converting the inputStream to a buffered one
imageData = new byte[bis.available()]; // initializing the byte array with the right size
bis.read(imageData); // reading / writing the image data
// HERE: if I comment out the next line, the second upload will upload the first image again.
// but if I set the Part to null, the second upload will result in nothing (page reloads, no data uploaded) and I have to upload it again
uploadedFile = null;
}
imageList.add(imageType + javax.xml.bind.DatatypeConverter.printBase64Binary(imageData)); // this adds the base64 image string to the list of images
}
}
My bean is #ViewScoped (I need it to be that way for other things).
So my guess was that the Part uploadedFile just didn't get the new image data for the second file, but as I said, when setting it to null it just skips the second upload process.
Does anyone see what I am doing wrong?
You might try the following.
byte[] imageData = new byte[uploadedFile.getSize()];
try (BufferedInputStream bis = new BufferedInputStream(inputStream)) {
bis.read(imageData);
uploadedFile.delete();
}
Using Part.getSize()
Not relying on available() which makes no guarantee that all is available
Using delete() to clear the bean
(Not tried it myself.)

Automatically open the printer dialog after providing PDF download

I am currently opening a pdf file in a new tab in my browser but I need to know how to open a printer dialog to print the pdf jasper report after pressing a commandButton
This is the method that open the pdf in a new tab:
public void printJasper() {
JasperReport compiledTemplate = null;
JRExporter exporter = null;
ByteArrayOutputStream out = null;
ByteArrayInputStream input = null;
BufferedOutputStream output = null;
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
HttpServletResponse response = (HttpServletResponse) externalContext.getResponse();
try {
List<String> sampleList = new ArrayList<String>();
sampleList.add("Fist sample string");
sampleList.add("Second sample string");
JRBeanCollectionDataSource beanCollectionDataSource = new JRBeanCollectionDataSource(sampleList);
Map<String, Object> reportValues = new HashMap<String, Object>();
reportValues.put("anyTestValue", "test value");
facesContext = FacesContext.getCurrentInstance();
externalContext = facesContext.getExternalContext();
response = (HttpServletResponse) externalContext.getResponse();
FileInputStream file = new FileInputStream("/any_dir/sample.jasper");
compiledTemplate = (JasperReport) JRLoader.loadObject(file);
out = new ByteArrayOutputStream();
JasperPrint jasperPrint = JasperFillManager.fillReport(compiledTemplate, reportValues, beanCollectionDataSource);
exporter = new JRPdfExporter();
exporter.setParameter(JRExporterParameter.JASPER_PRINT, jasperPrint);
exporter.setParameter(JRExporterParameter.OUTPUT_STREAM, out);
exporter.exportReport();
input = new ByteArrayInputStream(out.toByteArray());
response.reset();
response.setHeader("Content-Type", "application/pdf");
response.setHeader("Content-Length", String.valueOf(out.toByteArray().length));
response.setHeader("Content-Disposition", "inline; filename=\"fileName.pdf\"");
output = new BufferedOutputStream(response.getOutputStream(), Constants.DEFAULT_BUFFER_SIZE);
byte[] buffer = new byte[Constants.DEFAULT_BUFFER_SIZE];
int length;
while ((length = input.read(buffer)) > 0) {
output.write(buffer, 0, length);
}
output.flush();
} catch (Exception exception) {
/* ... */
} finally {
try {
if (output != null) {
output.close();
}
if (input != null) {
input.close();
}
} catch (Exception exception) {
/* ... */
}
}
facesContext.responseComplete();
}
This is the button that open the pdf file:
<p:commandButton action="#{sampleBB.printJasper}"
ajax="false" onclick="this.form.target='_blank'"
value="#{msg['generate.report']}" />
What I need to do?
With JasperReports
When using JasperReports, simply add this parameter to JasperReports exporter:
exporter.setParameter(JRPdfExporterParameter.PDF_JAVASCRIPT, "this.print();");
This basically instructs Adobe Acrobat to execute the script this.print() when opening the PDF. See also page 79-80 of Adobe Acrobat Scripting Guide. Below is an extract of relevance:
Printing PDF Documents
It is possible to use Acrobat JavaScript to specify whether a PDF document is sent to a
printer or to a PostScript file. In either case, to print a PDF document, invoke the doc
object’s print method. [...]
Without JasperReports
If you don't have control over generation of PDFs and thus can't manipulate it to add the mentioned script, an alternative is to change all the Java/JSF code accordingly so that the PDF file is idempotently available (i.e. the PDF file must be available by just a GET request rather than a POST request). This allows you to embed it in an <iframe> for which it's in turn possible to print its content by JavaScript during onload (keep CORS in mind though).
Simply put, the enduser must be able to download the desired PDF file by just entering its URL in browser's address bar. You can of course make use of GET request query string to specify parameters, allowing a bit more dynamicness. If it's "very large" data, then you can always let JSF put it in the HTTP session or DB and then pass an unique identifier around as request parameter so that the servlet can in turn obtain it from the very same HTTP session or DB.
Whilst possible with some nasty hacks, a JSF backing bean is simply insuitable for the job of idempotently serving a non-JSF response. You'd better use a "plain vanilla" servlet for this. You'll end up with much simpler code. Here's a kickoff example of such a servlet:
#WebServlet("/pdf")
public class PdfServlet extends HttpServlet {
#Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String foo = request.getParameter("foo");
String bar = request.getParameter("bar");
// ...
// Now just use the same code as in your original bean *without* FacesContext.
// Note that the HttpServletResponse is readily available as method argument!
response.setContentType("application/pdf");
// ...
}
}
With this setup, it's available by http://localhost:8080/context/pdf?foo=abc&bar=xyz.
Once you get that part to work, then you just have to reference it in an <iframe> which uses JavaScript to print its own content window during its load event. You can do this in a JSF page, e.g. /pdf.xhtml:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
>
<h:head>
<style>html, body { height: 100%; margin: 0; overflow: hidden; }</style>
</h:head>
<h:body>
<iframe src="#{request.contextPath}/pdf?#{request.queryString}"
width="100%" height="100%" onload="this.contentWindow.print()" />
</h:body>
</html>
All you need to do in your JSF backing bean is to send a redirect to that page, if necessary with parameters in request query string (which will end up in #{request.queryString} so that the servlet can obtain them via request.getParameter(...)).
Here's a kickoff example:
<h:form target="_blank">
<h:commandButton value="Generate report" action="#{bean.printPdf}" />
</h:form>
public String printPdf() {
// Prepare params here if necessary.
String foo = "abc";
String bar = "xyz";
// ...
return "/pdf?faces-redirect=true"
+ "&foo=" + URLEncoder.encode(foo, "UTF-8")
+ "&bar=" + URLEncoder.encode(bar, "UTF-8");
}
There is a <p:printer> Primefaces's component for that purpose.
Something like this might work, not tested though.
<h:form>
<h:commandButton value="Print" type="button" icon="ui-icon-print">
<p:printer target="pdf" />
</h:commandButton>
<p:media style="display:none;" id="pdf" value="/aPDF.pdf" />
</h:form>
Note :
<p:media> does have a print button to print the displayed pdf.
Edit :
You have to embed the pdf file inside an iframe and use the JavaScript print() function on it, or you can activate the auto-print function in the PDF itself. But it is definitely possible.
See this question on SO : Can a PDF file's print dialog be opened with Javascript?
How to Use JavaScript to Print a PDF
You can't print a URL directly from JavaScript, you can only open the print dialog for the existing page - article and print API.
PDF is generated on the server and sent to the web-browser (as a separate "page") which has to decide how to process it - the user is usually asked if it wants to display or save the PDF.
To "automatically print" (i.e. open a print dialog) an HTML page you would just have something like this:
window.onload = function() {
window.print();
};
But that can't be done for a PDF since it's not an HTML page.
To "automatically print" something other than an HTML page you would need to have a web-browser plug-in to handle PDFs from your server.
Another posibility is to write a GreaseMonkey user-script that would react on *.myserver.com/**.pdf and have it printed. Note: GreaseMonkey is a Mozilla Firefox plug-in.
Heavy weight option
You could accomplish your task by adding print support to your server application. Application requirements:
It would have to be an Intranet application (self-hosted inside the user's network),
Admin user would need to register network printers accessible from the server via a JSP page,
A "print dialog" page where you would select a registered printer and on clicking the "Print" button send a "print" request, for example:
/print?printer=printer1&doc=/reports/report1
I have seen a Java web application that supported this, but as you can see, it's not an easy task.
#Sujan Sivagurunathan
I tried combining the p:printer and p:media by replacing the image on the p:printer demo page with the PDF file from the p:media demo page:
// Replace this line:
<img id="j_idt18:image" src="/showcase/images/nature1.jpg?pfdrid_c=true" alt="">
// With this one:
<object **id="j_idt18:image"** style="display:none;" type="application/pdf" data="/showcase/resources/other/guide.pdf?pfdrid_c=true">Undefined</object>
When you click the Print button you get an empty page. If you omit the style="display:none;" and leave the PDF's height="300px" width="100%" you will get a small rectangle on the page print preview.
Eidt
Thank you, BalusC and Sujan. I agree, there is an option of embedding JavaScript inside the PDF, but that's usually disabled for security reasons.
I suppose the most browser-compatible and user-friendly way is to have a dedicated Print Preview pop-up window with an iframe showing the given PDF via GET request and a Print button to invoke the IFRAME's contentWindow.print().
It is generally a bad idea to just print a document without letting the user select the printer and configure it.

yui How to make in tag image does not necessarily will been to specify its size?

Good day.
I use script Imagecropper
Script:
<img src="http://test.com/img/1362244329.jpg" id="yui_img" height="768" width="1024">
<script>
(function() {
var Dom = YAHOO.util.Dom,
Event = YAHOO.util.Event;
var crop = new YAHOO.widget.ImageCropper('yui_img');
})();
</script>
result:
But if i do not specify the image size, then i get next(see image):
<img src="http://test.com/img/1362244329.jpg" id="yui_img">
result:
And if i specify the wrong picture size, the window will increase the portion of the image:
<img src="http://test.com/img/1362244329.jpg" id="yui_img" height="333" width="500">
result:
How to make in tag image does not necessarily will been to specify its size?
First of all I'd like to point you to YUI 3 since YUI 2 is no longer supported. You shouldn't write new code using YUI 2. There's an ImageCropper component I wrote for YUI 3 that works just like the YUI 2 version in the YUI Gallery: http://yuilibrary.com/gallery/show/imagecropper. Since it copies what the YUI 2 ImageCropper did, it shares these issues with the older version.
What to do when the size of the image isn't specified
The reason why you're getting a small ImageCropper is that you're creating the widget before the image has been fetched and so the browser doesn't know its size yet. What you can do is wait for the image's onload event. You can listen to that event and create the ImageCropper after it fires:
(function() {
var Dom = YAHOO.util.Dom,
Event = YAHOO.util.Event;
var yui_img = Dom.get('yui_img');
Event.addListener(yui_img, 'load', function () {
var crop = new YAHOO.widget.ImageCropper(yui_img);
});
})();
Why the ImageCropper doesn't work with images with the wrong size
Neither the YUI 2 ImageCropper nor my YUI 3 version work with images when they don't have the right size. The reason is that both use the background: url() CSS style for showing the image inside the crop area (the non-darkened part of the widget). CSS backgrounds don't let you use a resized/zoomed image.
I plan on using another strategy at some point for the YUI 3 version that will fix the issue. However, you need to keep in mind that the ImageCropper component is designed so that you send the crop coordinates to the server for it to actually crop the image. That means that if you have the wrong size set to the image, the coordinates that the image cropper returns with its getCropCoords method wouldn't be the coordinates that match with the full sized image. Instead you'd also have to send the server the size of the image you've been using and do extra math to crop the image correctly.
In conclusion, you shouldn't use the image with the wrong size. You can fix the size of the image in two ways:
Use the HTML5 naturalWidth and naturalHeight attributes of the image. Those return the real size of the image even if it's resized. Unfortunately these attributes are not yet supported by all browsers.
Create a new image with JS, set it the same src as the image you're using, listen to its load event and get that image's size.
Something like this:
(function () {
var Dom = YAHOO.util.Dom;
var yui_img = Dom.get('yui_img'),
new_img = new Image();
new_img.onload = function () {
yui_img.width = new_img.width;
yui_img.height = new_img.height;
// create the ImageCropper
};
new_img.src = yui_img.src;
}());
A YUI3 version
You can easily do all this with YUI3:
YUI().use('gallery-imagecropper', function (Y) {
var img = Y.one('#yui_img');
img.on('load', function () {
var cropper = new Y.ImageCroper({
srcNode: img,
width: img.get('width'),
height: img.get('height')
});
cropper.render();
});
});
Typo in code. Should be
var cropper = new Y.ImageCropper({
You missed a letter "p".

Resources