I'm working on a program that creates several pdf docs and puts different text in the same location in them.
Text should be placed in a particular area and if it doesn't fit it in width it should wrap. It also has a custom font and may be differently aligned in that area. It should be Vertically aligned to Top because when the area is laid out for three lines and I has only one, it should appear on the top. Finally, I need to preserve leading on the level of font-size.
It is important to be precise in text positioning (e.g. I need an upper left corner of "H" from "Hello world" to appear definitely at 0, 100).
Now, I'm using
canvas.showTextAligned(paragraph, 0, 300,
TextAlignment.valueOf(alignment),
VerticalAlignment.TOP);
However, when I try to implement it with different fonts it has a various offset from desired y = 300. Moreover, offset differ from font to font. For Helvetica (everywhere 50 fontSize is used) offset is about 13 px, for Oswald about 17 px and for SedgwickAveDisplay it is massive 90 px.
I added borders to paragraph for debugging purpose and things become more strange.
Helvetica:
SedgwickAveDisplay:
The full snippet of my code to create pdf is below:
public byte[] createBadgeInMemory(int i) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
PdfDocument newPdf = new PdfDocument(new PdfWriter(out));
srcPdf.copyPagesTo(1,1,newPdf);
PdfPage page = newPdf.getFirstPage();
PdfCanvas pdfCanvas = new PdfCanvas(page);
Canvas canvas = new Canvas(pdfCanvas, newPdf, pageSize);
File defaultFont = new File("src/main/resources/fonts/Helvetica.otf");
PdfFont font = PdfFontFactory
.createFont(fontPath == null ? defaultFont.getAbsolutePath() : fontPath,
PdfEncodings.IDENTITY_H, true);
String value = "Example word";
Paragraph paragraph = new Paragraph(value);
float textWidth = font.getWidth("Example", 50);
paragraph.setWidth(textWidth);
switch (alignment) {
case("CENTER"):
textWidth /= 2;
break;
case("RIGHT"):
break;
default:
textWidth = 0;
break;
}
paragraph.setFont(font)
.setFontSize(fontSize)
.setFixedLeading(fontSize)
.setFontColor(new DeviceRgb(red, green, blue))
.setMargin(0)
.setPadding(0);
paragraph.setBorderTop(new DashedBorder(Color.BLACK, 0.5f))
.setBorderBottom(new DashedBorder(Color.BLACK, 0.5f))
.setBorderRight(new DashedBorder(Color.BLACK, 0.5f));
paragraph.setHyphenation(new HyphenationConfig(0,
"Example".length()));
canvas.showTextAligned(paragraph,
0 + textWidth,
300,
TextAlignment.valueOf(alignment),
VerticalAlignment.TOP);
newPdf.close();
return out.toByteArray();
}
I also tried variant from here, but for some reason text inside rectangle cuts out at some point (for instance, if I have area width 100px and text snippet I put in that I know occupies exactly 100 px (with the help of font.getWidth(value)), I have my text cut at nearly 80 px).
I also haven't found a way to align text inside a rectangle.
This is the result with Rectangle. A solid border is Rectangle border. As you can see it cuts letter "t" in "Redundant". It also should contain "word" on the second line, but it doesn't.
I copied code from an example.
I need your help. What am I doing wrong or may be there is another way to layout text in particular area with alignment and font?
Thank you in advance.
Update 21.09.17
Also tried variant from this question with SedgwickAveDisplay:
paragraph.setHorizontalAlignment(HorizontalAlignment.LEFT);
paragraph.setVerticalAlignment(VerticalAlignment.TOP);
paragraph.setFixedPosition( 0, 300 - textHeight, "Example".length());
doc.add(paragraph);
The result is the same as on the second screenshot.
This is a font-specific problem. iText guesses information about font glyphs, namely the bboxes incorrectly.
There is a quick and dirty method to adjust this behavior. You can create a custom renderer for text and adjust the calculated positions in it. An example of such a class would be as follows:
class CustomTextRenderer extends TextRenderer {
private CustomTextRenderer(Text text) {
super(text);
}
#Override
public LayoutResult layout(LayoutContext layoutContext) {
LayoutResult result = super.layout(layoutContext);
Rectangle oldBbox = this.occupiedArea.getBBox().clone();
// you can also make the height more than font size or less if needed
this.occupiedArea.setBBox(oldBbox.moveUp(oldBbox.getHeight() - fontSize).setHeight(fontSize));
yLineOffset = fontSize * 0.8f; // here you config the proportion of the ascender
return result;
}
#Override
public IRenderer getNextRenderer() {
return new CustomTextRenderer((Text) modelElement);
}
}
In order that new rendering logic to be applied, you have to use it in the following way:
Text text = new Text(value);
text.setNextRenderer(new CustomTextRenderer(text));
Paragraph paragraph = new Paragraph(text);
Please note that you have to be very careful with this kind of low-level layouting, be aware of you are doing and use this as seldom as possible.
Finally, I created a variant that worked for me.
pdfCanvas.beginText()
.setFontAndSize(font, fontSize)
.setLeading(fontSize)
.moveText(0, 300);
numberOfLines = 0;
sumOfShifts = 0;
float maxWidth = computeStringWidth("Exaxple");
String[] words = value.split("\\s");
StringBuilder line = new StringBuilder();
line.append(words[0]);
float spaceWidth = computeStringWidth(" ") ;
float lineWidth;
for (int index = 1; index < words.length; index++) {
String word = words[index];
float wordWidth = computeStringWidth(word) ;
lineWidth = computeStringWidth(line.toString()) ;
if (lineWidth + spaceWidth + wordWidth <= maxWidth) {
line.append(" ").append(word);
} else {
showTextAligned(alignment, pdfCanvas, line.toString(), lineWidth, maxWidth);
line.delete(0, line.length());
line.append(word);
}
}
if(line.length() != 0) {
lineWidth = computeStringWidth(line.toString()) ;
showTextAligned(alignment, pdfCanvas, line.toString(), lineWidth, maxWidth);
}
pdfCanvas.endText();
As computeStringWidth(String str) I used
Toolkit.getToolkit().getFontLoader().computeStringWidth(String str, Font font);
from import com.sun.javafx.tk.Toolkit with Font from javafx.scene.text.Font. I've chosen it because I use it in other parts of my app.
showTextAligned(...) is my own method that looks this way:
private void showTextAligned(String alignment,
PdfCanvas pdfCanvas,
String line,
float lineWidth,
float maxWidth) {
switch (alignment) {
case "CENTER": {
float shift = (maxWidth - lineWidth) / 2 - sumOfShifts;
pdfCanvas.moveText(shift, 0);
if(numberOfLines == 0) pdfCanvas.showText(line);
else pdfCanvas.newlineShowText(line);
numberOfLines++;
sumOfShifts += shift;
break;
}
case "RIGHT": {
float shift = maxWidth - lineWidth - sumOfShifts;
pdfCanvas.moveText(shift, 0);
if(numberOfLines == 0) pdfCanvas.showText(line);
else pdfCanvas.newlineShowText(line);
numberOfLines++;
sumOfShifts += shift;
break;
}
default:
pdfCanvas.moveText(0, 0);
if(numberOfLines == 0) pdfCanvas.showText(line);
else pdfCanvas.newlineShowText(line);
numberOfLines++;
break;
}
}
In my project, I used my variant, because it gives me an opportunity to work with hyphenation deeper (for instance, I can in future add functionality to avoid putting prepositions as a last word in the line).
Related
I have 11 files with over 140 slides each, and none of the shapes are tied to a theme/master. My goal was the change the master font and also replace all the red text (there is SO much red text) with black text.
I successfully updated the master font (thanks to https://gist.github.com/dsottimano/20a50daded2128b4c86acb430cecba67), but have come up short in trying to write something for ForegoundColor.
I tried to adapt this code and cannot make it work: https://mashe.hawksey.info/2017/10/changing-the-color-of-all-links-in-google-slides-with-google-apps-script/
I need to replace all text formatted with foregroundcolor hex #e04935 with hex #000000.
Appreciate any tips on making this work!
Here is what I have done so far:
//original from: https://mashe.hawksey.info/2017/10/changing-the-color-of-all-links-in-google-slides-with-google-apps-script/
/**
* #OnlyCurrentDoc
*/
function changeTextColorforShapes(){
var deck = SlidesApp.getActivePresentation();
var slides = deck.getSlides();
slides.forEach(function(slide) {
// https://developers.google.com/apps-script/reference/slides/slide#getPageElements()
var elements = slide.getPageElements();
elements.forEach(function(element){
// https://developers.google.com/apps-script/reference/slides/page-element#getPageElementType()
var type = element.getPageElementType();
// Text boxes are 'SHAPES' (this code doesn't handle table cells)
if (type == "SHAPE"){
// https://developers.google.com/apps-script/reference/slides/text-range#getTextStyle()
var textStyle = element.asShape().getText().getForegroundColor();
// https://developers.google.com/apps-script/reference/slides/text-style#hasLink()
// Returns true if text is color #e04935 (https://www.color-hex.com/color/e04935) and changes text to color #ffffff (black)
if (ForegroundColor('#e04935')
textStyle.setForegroundColor('#ffffff');
}
});
});
}
If you want to change all the text from #E04935 to #000000, you can try the below snippet:
Snippet
// Copyright 2020 Google LLC.
// SPDX-License-Identifier: Apache-2.0
function changeColor() {
var presentation = SlidesApp.getActivePresentation();
var slides = presentation.getSlides();
for (let i = 0; i < slides.length; i++) {
var elements = slides[i].getPageElements();
for (let j = 0; j < elements.length; j++)
if (elements[j].asShape().getText().getTextStyle().getForegroundColor().asRgbColor().asHexString() == '#E04935')
elements[j].asShape().getText().getTextStyle().setForegroundColor('#000000');
}
}
Explanation
The reason your code was not working is because you were trying to use the getForegroundColor() method on an object of type TextRange while this method is expected to be called from an object of type TextStyle.
So in order to test exactly if the color of the text is the wanted one, you will have to retrieve first the color as a RGB color in order to get the HEX value of it.
Reference
Apps Script Editing & Styling Text Slides;
Apps Script Class TextStyle - getForegroundColor().
As you can see, I've got a table which sports borders and a background color (Section 1):
...but section 3 lacks these sartorial refinements, and I don't know why.
Here is the pertinent code for section 1 (which displays as it should):
PdfPTable tblHeadings = new PdfPTable(3);
tblHeadings.WidthPercentage = 100;
tblHeadings.SpacingBefore = 10f;
float[] headingRowWidths = new float[] { 550f, 50f, 400f };
tblHeadings.SetWidths(headingRowWidths);
tblHeadings.HorizontalAlignment = Element.ALIGN_LEFT;
Phrase phrasesec1Heading = new Phrase("Section 1: Payment Information", timesRoman9BoldFont);
PdfPCell cellSec1Heading GetCellForBorderedTable(phrasesec1Heading, Element.ALIGN_LEFT, ucscgold);
tblHeadings.AddCell(cellSec1Heading);
. . .
doc.Add(tblHeadings);
...and here for section 3 (which doesn't display as it should):
// Section 3
PdfPTable tblSection3 = new PdfPTable(1);
tblSection3.WidthPercentage = 100;
tblSection3.SpacingBefore = 10f;
//tblSection3.HorizontalAlignment = Element.ALIGN_LEFT;
Chunk sec3PayeeStatus = new Chunk("Section 3: Payee Status", timesRoman9BoldFont);
Chunk requiredFields = new Chunk(" * Required Fields", timesRoman9BoldRedFont);
Paragraph parSection3 = new Paragraph();
parSection3.Add(sec3PayeeStatus);
parSection3.Add(requiredFields);
//Phrase phrasesec1Heading = new Phrase("Section 1: Payment Information", timesRoman9BoldFont);
PdfPCell cellSec3Heading = GetCellForBorderedTable(parSection3, Element.ALIGN_LEFT, ucscgold);
tblSection3.AddCell(cellSec3Heading);
doc.Add(parSection3);
What am I missing or forgetting? The difference in the table creation and setup is in the number of cells/columns, and related declaration (float array) and property (setWidths()). The code adding to the PdfPCell differs in that a Phrase
is used for Section 1 (which displays as I want it to), and a Paragraph is used for section 3 (which doesn't), but I tried that:
// try using a Phrase instead of a Paragraph
Chunk sec3PayeeStatus = new Chunk("Section 3 PayeeStatus",
timesRoman9BoldFont);
Chunk requiredFields = new Chunk(" * Require Fields",
timesRoman9BoldRedFont);
Phrase phraseSection3 = new Phrase();
phraseSection3.Add(sec3PayeeStatus);
phraseSection3.Add(requiredFields);
PdfPCell cellSec3Heading GetCellForBorderedTable(phraseSection3,
Element.ALIGN_LEFT, ucscgold);
tblSection3.AddCell(cellSec3Heading);
doc.Add(phraseSection3);
...but that did no better, in fact was a little worse, because the "SpacingBefore" didn't seem to "take"
Finally (so far), I tried using Phrases instead of Chunks, like so:
Phrase sec3PayeeStatus = new Phrase("Section 3: Payee Status", timesRoman9BoldFont);
Phrase requiredFields = new Phrase(" * Required Fields", timesRoman9BoldRedFont);
Paragraph parSection3 = new Paragraph();
parSection3.Add(sec3PayeeStatus);
parSection3.Add(requiredFields);
PdfPCell cellSec3Heading = GetCellForBorderedTable(parSection3, Element.ALIGN_LEFT, ucscgold);
tblSection3.AddCell(cellSec3Heading);
doc.Add(parSection3);
...but that is no better, worse, or different than the first attempt (at least I get the vertical space back, though, separating sections 2 and 3).
What do I have to do to display the cell borders and background color?
UPDATE
Here's GetCellForBorderedTable():
private static PdfPCell GetCellForBorderedTable(Phrase phrase, int align, BaseColor color)
{
PdfPCell cell = new PdfPCell(phrase);
cell.HorizontalAlignment = align;
cell.PaddingBottom = 2f;
cell.PaddingTop = 0f;
cell.BackgroundColor = color;
cell.VerticalAlignment = PdfPCell.ALIGN_CENTER;
return cell;
}
The problem was (not "visible" to me until I compared the code in a code comparison utility, namely KDiff3) that I was adding the Paragraph, not the table, to the doc:
doc.Add(parSection3);
Now that I've changed that to:
doc.Add(tblSection3);
...it works.
I want to float an image on the right of some text in a table cell.
cell.AddParagraph("some text");
cell.AddParagraph("next line other text");
Paragraph p = cell.AddParagraph();
p.Format.Alignment = ParagraphAlignment.Right;
p.Format.RightIndent = 12;
Image image = p.AddImage("image.png");
image.ScaleWidth = 0.07;
This code puts the image to the right, but I can't move it up.
p.Format.Linespacing = -10;
image.WrapFormat.DistanceTop = -10;
These settings did not work.
For moving elements up or down SpaceBefore and SpaceAfter can be used.
In this case:
p.Format.SpaceBefore = - 20;
I try to create game options but got problem with cell size adjustment.For option "First move" I use names of users. I want to increase cell if name is too long. Or decrease font size if it easier.
Style for cells:
mCellStyle = new GUIStyle();//style for cells
mCellStyle.normal.background = Resources.Load<Texture2D>("Textures/button_up_9patch");
mCellStyle.onNormal.background = Resources.Load<Texture2D>("Textures/button_down_9patch");
mCellStyle.focused.background = mButtonStyle.active.background;
mCellStyle.fontSize = GUIUtils.GetKegel() - GUIUtils.GetKegel() / 5;
mCellStyle.border = new RectOffset(7, 7, 7, 7);
mCellStyle.padding = new RectOffset(20, 20, 20, 20);
mCellStyle.alignment = TextAnchor.MiddleCenter;
mCellStyle.wordWrap = true;
My code:
GUIStyle lBackStyle = new GUIStyle(mCellStyle);
lBackStyle.fontSize = GUIUtils.GetKegel();
lBackStyle.active.background = null;
lBackStyle.focused.background = null;
GUIStyle lStyle = new GUIStyle(lBackStyle);
lStyle.normal.background = null;
//create contents and calculate height
GUIContent lContent1 = new GUIContent(LanguageManager.GetText("FirstMove"));
float lElemH1 = lStyle.CalcHeight(lContent1, lMaxWidht);
GUIContent[] lArrayContent2 = new GUIContent[] { new GUIContent(MainScript.Logic.UserName), new GUIContent(MainScript.Logic.NurslingName) };
float lElemH2 = mCellStyle.CalcHeight(lArrayContent2[0], lMaxWidht);
GUIContent lContent3 = new GUIContent(LanguageManager.GetText("Difficulty"));
float lElemH3 = lStyle.CalcHeight(lContent3, lMaxWidht);
GUIContent[] lArrayContent4 = new GUIContent[] { new GUIContent(LanguageManager.GetText("Easy")), new GUIContent(LanguageManager.GetText("Hard")) };
float lElemH4 = mCellStyle.CalcHeight(lArrayContent4[0], lMaxWidht);
float lTotalH = lElemH1 + lElemH2 + lElemH3 /*+ 100*/;//reserve 100 for paddings
GUILayout.BeginArea(mAreaRect);
GUILayout.BeginVertical(lBackStyle, GUILayout.Height(lTotalH));
GUILayout.Label(lContent1, lStyle);
GamePreferences.setAIMakesFirstMove(GUILayout.SelectionGrid(GamePreferences.getAIMakesFirstMove(), lArrayContent2, 2, mCellStyle));
GUILayout.Label(lContent3, lStyle);
GamePreferences.setDifficulty(GUILayout.SelectionGrid(GamePreferences.getDifficulty(), lArrayContent4, 2, mCellStyle));
GUILayout.EndVertical();
GUILayout.EndArea();
ADDED: I just want to set height of cell in Selection Grid. Is it possible?
Ok, I found that I can pass GUILayoutOption as last parameter to set the height of cells:
GUILayout.SelectionGrid(GamePreferences.getFirstMove(), lArrayContent2, 2, mCellStyle, GUILayout.Height(lCellHeight))
But I still have no idea how I can fit cell size to it content.
float lElemH2 = mCellStyle.CalcHeight(lArrayContent2[0], lMaxWidht);
mCellStyle.CalcHeight ignores lenght of content and in lElemH2 I got height for single line text.
EDITED:
My fault, I sent wrong parameters to CalcHeight. Correct version:
float lElemH2 = mCellStyle.CalcHeight(lArrayContent2[0], lMaxWidht / cellsCount);
In JavaFX, how to get the cell renderer instance for a given cell of a given TableColumn?
In Swing, the way to do it would be to invoke getTableCellRendererComponent() on the TableCellRenderer for that column and pass it the row and column indices. But JavaFX seems to be very different. I tried searching and going through the TableColumn API but I don't seem to be able to figure this out. Maybe I have to do something with getCellFactory().
My goal is to query the preferred width for each cell renderer of a column and then calculate the width to set on the column so that the contents of all the cells of that column are fully visible.
A question was asked here - JavaFX 2 Automatic Column Width - where the goal of the original poster was the same as that of mine. But there hasn't been a satisfactory answer there yet.
There is resizeToFit() method in TableColumnHeader class. Unfortunately it is protected. How about just copy-paste the code to your application and change it a bit:
protected void resizeToFit(TableColumn col, int maxRows) {
List<?> items = tblView.getItems();
if (items == null || items.isEmpty()) return;
Callback cellFactory = col.getCellFactory();
if (cellFactory == null) return;
TableCell cell = (TableCell) cellFactory.call(col);
if (cell == null) return;
// set this property to tell the TableCell we want to know its actual
// preferred width, not the width of the associated TableColumn
cell.getProperties().put("deferToParentPrefWidth", Boolean.TRUE);//the change is here, only the first parameter, since the original constant is not accessible outside package
// determine cell padding
double padding = 10;
Node n = cell.getSkin() == null ? null : cell.getSkin().getNode();
if (n instanceof Region) {
Region r = (Region) n;
padding = r.getInsets().getLeft() + r.getInsets().getRight();
}
int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows);
double maxWidth = 0;
for (int row = 0; row < rows; row++) {
cell.updateTableColumn(col);
cell.updateTableView(tblView);
cell.updateIndex(row);
if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
getChildren().add(cell);
cell.impl_processCSS(false);
maxWidth = Math.max(maxWidth, cell.prefWidth(-1));
getChildren().remove(cell);
}
}
col.impl_setWidth(maxWidth + padding);
}
Then you can call the method after load data:
for (TableColumn clm : tblView.getColumns()) {
resizeToFit(clm, -1);
}