Compare commits
21 commits
Author | SHA1 | Date | |
---|---|---|---|
|
b2f39378e8 | ||
|
4610003595 | ||
|
86c3cdac75 | ||
|
d2df552fa2 | ||
|
842296af3e | ||
|
40c2d99c47 | ||
|
724b724272 | ||
|
e11a6a1416 | ||
|
72309329c0 | ||
|
07ef236658 | ||
|
a5ce8b546b | ||
|
d5dfccab17 | ||
|
e827b42df3 | ||
9cece23736 | |||
|
8c3c5f22c1 | ||
|
25ea1c6554 | ||
|
e8317aaa9e | ||
|
ee5f8ba78c | ||
|
d3561c9b21 | ||
|
c3f2b6c619 | ||
272fe20e4c |
20 changed files with 243 additions and 746 deletions
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: maven
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 10
|
34
.github/release-drafter.yml
vendored
Normal file
34
.github/release-drafter.yml
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
name-template: 'v$RESOLVED_VERSION 🌈'
|
||||||
|
tag-template: 'v$RESOLVED_VERSION'
|
||||||
|
categories:
|
||||||
|
- title: '🚀 Features'
|
||||||
|
labels:
|
||||||
|
- 'feature'
|
||||||
|
- 'enhancement'
|
||||||
|
- title: '🐛 Bug Fixes'
|
||||||
|
labels:
|
||||||
|
- 'fix'
|
||||||
|
- 'bugfix'
|
||||||
|
- 'bug'
|
||||||
|
- title: '🧰 Maintenance'
|
||||||
|
labels:
|
||||||
|
- 'chore'
|
||||||
|
- 'dependencies'
|
||||||
|
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
||||||
|
version-resolver:
|
||||||
|
major:
|
||||||
|
labels:
|
||||||
|
- 'major'
|
||||||
|
minor:
|
||||||
|
labels:
|
||||||
|
- 'minor'
|
||||||
|
patch:
|
||||||
|
labels:
|
||||||
|
- 'patch'
|
||||||
|
default: patch
|
||||||
|
exclude-labels:
|
||||||
|
- 'skip-changelog'
|
||||||
|
template: |
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
$CHANGES
|
17
.github/stale.yaml
vendored
Normal file
17
.github/stale.yaml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Number of days of inactivity before an issue becomes stale
|
||||||
|
daysUntilStale: 60
|
||||||
|
# Number of days of inactivity before a stale issue is closed
|
||||||
|
daysUntilClose: 7
|
||||||
|
# Issues with these labels will never be considered stale
|
||||||
|
exemptLabels:
|
||||||
|
- pinned
|
||||||
|
- security
|
||||||
|
# Label to use when marking an issue as stale
|
||||||
|
staleLabel: wontfix
|
||||||
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs. Thank you
|
||||||
|
for your contributions.
|
||||||
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
|
closeComment: false
|
23
.github/workflows/build-maven.yml
vendored
Normal file
23
.github/workflows/build-maven.yml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
name: maven-build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: '*'
|
||||||
|
pull_request:
|
||||||
|
branches: '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
java: [ 11, 14 ]
|
||||||
|
steps:
|
||||||
|
- uses: kamiazya/setup-graphviz@v1
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: setup-jdk-${{ matrix.java }}
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: ${{ matrix.java }}
|
||||||
|
- name: build-jar
|
||||||
|
run: mvn -B install
|
|
@ -1,19 +1,20 @@
|
||||||
name: Deploy to Sonatype Nexus
|
name: sonatype-deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
push:
|
||||||
types: [created]
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# - uses: kamiazya/setup-graphviz@v1
|
- uses: kamiazya/setup-graphviz@v1
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 8
|
||||||
- name: Build with Maven
|
- name: Build with Maven
|
||||||
run: mvn -B install
|
run: mvn -B install
|
||||||
- name: Nexus Repo Publish
|
- name: Nexus Repo Publish
|
14
.github/workflows/draft-release.yml
vendored
Normal file
14
.github/workflows/draft-release.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
name: draft-release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update_draft_release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: release-drafter/release-drafter@v5.11.0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
26
.github/workflows/maven-build.yml
vendored
26
.github/workflows/maven-build.yml
vendored
|
@ -1,26 +0,0 @@
|
||||||
# This workflow will build a Java project with Maven
|
|
||||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
|
|
||||||
|
|
||||||
name: Java CI with Maven
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: '*'
|
|
||||||
pull_request:
|
|
||||||
branches: '*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
java: [ 11, 13 ]
|
|
||||||
steps:
|
|
||||||
# - uses: kamiazya/setup-graphviz@v1
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: setup-java-${{ matrix.java }}
|
|
||||||
uses: actions/setup-java@v1
|
|
||||||
with:
|
|
||||||
java-version: ${{ matrix.java }}
|
|
||||||
- name: install
|
|
||||||
run: mvn -B install
|
|
|
@ -14,6 +14,10 @@ font size and/or overflow into additional rectangles.
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* JDK 11+
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Word Wrap
|
### Word Wrap
|
||||||
|
|
12
pom.xml
12
pom.xml
|
@ -14,17 +14,13 @@
|
||||||
<version>DEV-SNAPSHOT</version>
|
<version>DEV-SNAPSHOT</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>11</java.version>
|
<tiles-maven-plugin.version>2.21</tiles-maven-plugin.version>
|
||||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
|
||||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
|
||||||
|
|
||||||
<tiles-maven-plugin.version>2.16</tiles-maven-plugin.version>
|
|
||||||
<kemitix-maven-tiles.version>2.6.0</kemitix-maven-tiles.version>
|
<kemitix-maven-tiles.version>2.6.0</kemitix-maven-tiles.version>
|
||||||
<kemitix-checkstyle.version>5.4.0</kemitix-checkstyle.version>
|
<kemitix-checkstyle.version>5.4.0</kemitix-checkstyle.version>
|
||||||
<lombok.version>1.18.12</lombok.version>
|
<lombok.version>1.18.20</lombok.version>
|
||||||
|
|
||||||
<junit.version>5.6.2</junit.version>
|
<junit.version>5.7.2</junit.version>
|
||||||
<assertj.version>3.16.0</assertj.version>
|
<assertj.version>3.19.0</assertj.version>
|
||||||
<mockito.version>3.3.3</mockito.version>
|
<mockito.version>3.3.3</mockito.version>
|
||||||
<jqwik.version>1.2.7</jqwik.version>
|
<jqwik.version>1.2.7</jqwik.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
|
@ -2,7 +2,6 @@ package net.kemitix.text.fit;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.geom.Rectangle2D;
|
import java.awt.geom.Rectangle2D;
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
public interface BoxFitter {
|
public interface BoxFitter {
|
||||||
|
@ -12,10 +11,4 @@ public interface BoxFitter {
|
||||||
Graphics2D graphics2D,
|
Graphics2D graphics2D,
|
||||||
Rectangle2D box
|
Rectangle2D box
|
||||||
);
|
);
|
||||||
int fit(
|
|
||||||
String text,
|
|
||||||
Function<Integer, Font> fontFactory,
|
|
||||||
Graphics2D graphics2D,
|
|
||||||
List<Rectangle2D> box
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,9 @@ import lombok.RequiredArgsConstructor;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.font.FontRenderContext;
|
import java.awt.font.FontRenderContext;
|
||||||
import java.awt.geom.Rectangle2D;
|
import java.awt.geom.Rectangle2D;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.BiFunction;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fit the text to a box by finding the best font size and word wrapping to fill
|
* Fit the text to a box by finding the best font size and word wrapping to fill
|
||||||
|
@ -19,7 +16,6 @@ import java.util.stream.Stream;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
class BoxFitterImpl implements BoxFitter {
|
class BoxFitterImpl implements BoxFitter {
|
||||||
|
|
||||||
public static final int MAX_FONT_SIZE = 10_000;
|
|
||||||
private final WordWrapper wordWrapper;
|
private final WordWrapper wordWrapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -29,22 +25,10 @@ class BoxFitterImpl implements BoxFitter {
|
||||||
Graphics2D graphics2D,
|
Graphics2D graphics2D,
|
||||||
Rectangle2D box
|
Rectangle2D box
|
||||||
) {
|
) {
|
||||||
return fit(text, fontFactory, graphics2D,
|
int fit = fitMinMax(0, (int) box.getHeight(),
|
||||||
Collections.singletonList(box));
|
new FitEnvironment(text, fontFactory, graphics2D, box));
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int fit(
|
|
||||||
String text,
|
|
||||||
Function<Integer, Font> fontFactory,
|
|
||||||
Graphics2D graphics2D,
|
|
||||||
List<Rectangle2D> boxes
|
|
||||||
) {
|
|
||||||
int fit = fitMinMax(0, MAX_FONT_SIZE,
|
|
||||||
new FitEnvironment(text, fontFactory, graphics2D,
|
|
||||||
boxes));
|
|
||||||
if (fit <= 2) {
|
if (fit <= 2) {
|
||||||
throw new NotEnoughSpace(0);
|
throw new IllegalArgumentException("The text is too long to fit");
|
||||||
}
|
}
|
||||||
return fit;
|
return fit;
|
||||||
}
|
}
|
||||||
|
@ -55,39 +39,25 @@ class BoxFitterImpl implements BoxFitter {
|
||||||
FitEnvironment e
|
FitEnvironment e
|
||||||
) {
|
) {
|
||||||
int mid = (max + min) / 2;
|
int mid = (max + min) / 2;
|
||||||
if (mid == min && mid + 1 == max) {
|
if (mid == min){
|
||||||
return mid;
|
return mid;
|
||||||
}
|
}
|
||||||
Font font = e.getFont(mid);
|
Font font = e.getFont(mid);
|
||||||
try {
|
List<String> lines = wrapLines(font, e);
|
||||||
List<List<Rectangle2D>> linesPerBox = wrapLines(font, e);
|
List<Rectangle2D> lineSizes =
|
||||||
if (tooManyLines(linesPerBox, e.boxes)) {
|
lineSizes(font, lines, e.fontRenderContext());
|
||||||
return fitMinMax(min, mid, e);
|
if (sumLineHeights(lineSizes) > e.boxHeight() ||
|
||||||
}
|
maxLineWidth(lineSizes) > e.boxWidth()) {
|
||||||
} catch (WordTooLong | NotEnoughSpace err) {
|
|
||||||
return fitMinMax(min, mid, e);
|
return fitMinMax(min, mid, e);
|
||||||
}
|
}
|
||||||
return fitMinMax(mid, max, e);
|
return fitMinMax(mid, max, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean tooManyLines(
|
private List<String> wrapLines(
|
||||||
List<List<Rectangle2D>> linesPerBox,
|
|
||||||
List<Rectangle2D> boxes
|
|
||||||
) {
|
|
||||||
return StreamZipper.zip(linesPerBox, boxes,
|
|
||||||
(stringBounds, box) ->
|
|
||||||
sumLineHeights(stringBounds) > box.getHeight()
|
|
||||||
).anyMatch(b -> b);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<List<Rectangle2D>> wrapLines(
|
|
||||||
Font font,
|
Font font,
|
||||||
FitEnvironment e
|
FitEnvironment e
|
||||||
) {
|
) {
|
||||||
return wordWrapper.wrap(e.text, font, e.graphics2D, e.boxes)
|
return wordWrapper.wrap(e.text, font, e.graphics2D, e.boxWidth());
|
||||||
.stream()
|
|
||||||
.map(l -> lineSizes(font, l, e.fontRenderContext()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Rectangle2D> lineSizes(
|
private List<Rectangle2D> lineSizes(
|
||||||
|
@ -101,10 +71,13 @@ class BoxFitterImpl implements BoxFitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private int sumLineHeights(List<Rectangle2D> lineSizes) {
|
private int sumLineHeights(List<Rectangle2D> lineSizes) {
|
||||||
return lineSizes.stream()
|
return lineSizes.stream().map(Rectangle2D::getHeight)
|
||||||
.map(Rectangle2D::getHeight)
|
.mapToInt(Double::intValue).sum();
|
||||||
.mapToInt(Double::intValue)
|
}
|
||||||
.sum();
|
|
||||||
|
private int maxLineWidth(List<Rectangle2D> lineSizes) {
|
||||||
|
return lineSizes.stream().map(Rectangle2D::getWidth)
|
||||||
|
.mapToInt(Double::intValue).max().orElse(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@ -112,11 +85,18 @@ class BoxFitterImpl implements BoxFitter {
|
||||||
private final String text;
|
private final String text;
|
||||||
private final Function<Integer, Font> fontFactory;
|
private final Function<Integer, Font> fontFactory;
|
||||||
private final Graphics2D graphics2D;
|
private final Graphics2D graphics2D;
|
||||||
private final List<Rectangle2D> boxes;
|
private final Rectangle2D box;
|
||||||
|
|
||||||
public Font getFont(int size) {
|
public Font getFont(int size) {
|
||||||
return fontFactory.apply(size);
|
return fontFactory.apply(size);
|
||||||
}
|
}
|
||||||
|
public int boxWidth() {
|
||||||
|
return (int) box.getWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int boxHeight() {
|
||||||
|
return (int) box.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
public FontRenderContext fontRenderContext() {
|
public FontRenderContext fontRenderContext() {
|
||||||
return graphics2D.getFontRenderContext();
|
return graphics2D.getFontRenderContext();
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
package net.kemitix.text.fit;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class NotEnoughSpace extends RuntimeException {
|
|
||||||
private final int excessWordCount;
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
package net.kemitix.text.fit;
|
|
||||||
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.BiFunction;
|
|
||||||
import java.util.function.IntFunction;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static java.lang.Math.min;
|
|
||||||
import static java.util.stream.IntStream.range;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to zip two {@link Stream}s together.
|
|
||||||
*
|
|
||||||
* @author Paul Campbell (pcampbell@kemitix.net)
|
|
||||||
*/
|
|
||||||
public final class StreamZipper {
|
|
||||||
|
|
||||||
private StreamZipper() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zip two {@link Stream}s together.
|
|
||||||
*
|
|
||||||
* <p>The resulting stream will contain only as many items as the shortest of the two lists.</p>
|
|
||||||
*
|
|
||||||
* @param a the first List
|
|
||||||
* @param b the second List
|
|
||||||
* @param zipper the function to zip an item from each list
|
|
||||||
* @param <A> the type of the first list
|
|
||||||
* @param <B> the type of the second list
|
|
||||||
* @param <C> the type of the joined items
|
|
||||||
* @return a Stream of the joined items
|
|
||||||
*/
|
|
||||||
public static <A, B, C> Stream<C> zip(
|
|
||||||
final List<A> a,
|
|
||||||
final List<B> b,
|
|
||||||
final BiFunction<A, B, C> zipper
|
|
||||||
) {
|
|
||||||
return range(0, limit(a, b))
|
|
||||||
.mapToObj(tuple(a, b, zipper));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <A, B> int limit(
|
|
||||||
final List<A> a,
|
|
||||||
final List<B> b
|
|
||||||
) {
|
|
||||||
return min(a.size(), b.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <A, B, C> IntFunction<C> tuple(
|
|
||||||
final List<A> a,
|
|
||||||
final List<B> b,
|
|
||||||
final BiFunction<A, B, C> zipper
|
|
||||||
) {
|
|
||||||
return i -> zipper.apply(a.get(i), b.get(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,11 +1,10 @@
|
||||||
package net.kemitix.text.fit;
|
package net.kemitix.text.fit;
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.font.FontRenderContext;
|
import java.awt.font.FontRenderContext;
|
||||||
import java.awt.geom.Rectangle2D;
|
import java.awt.geom.Rectangle2D;
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -17,78 +16,31 @@ class TextLineWrapImpl implements WordWrapper {
|
||||||
Font font,
|
Font font,
|
||||||
Graphics2D graphics2D,
|
Graphics2D graphics2D,
|
||||||
int width
|
int width
|
||||||
) {
|
|
||||||
return wrap(text, font, graphics2D,
|
|
||||||
Collections.singletonList(
|
|
||||||
new Rectangle(width, Integer.MAX_VALUE)))
|
|
||||||
.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<List<String>> wrap(
|
|
||||||
String text,
|
|
||||||
Font font,
|
|
||||||
Graphics2D graphics2D,
|
|
||||||
List<Rectangle2D> boxes
|
|
||||||
) {
|
) {
|
||||||
String source = String.join(" ", text.split("\n"));
|
String source = String.join(" ", text.split("\n"));
|
||||||
List<Word> words = wordLengths(source.split(" "), font, graphics2D);
|
List<Word> words = wordLengths(source.split(" "), font, graphics2D);
|
||||||
return wrapWords(words, boxes);
|
return wrapWords(words, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<List<String>> wrapWords(
|
private List<String> wrapWords(List<Word> words, int width) {
|
||||||
List<Word> words,
|
List<String> lines = new ArrayList<>();
|
||||||
List<Rectangle2D> boxes
|
int end = 0;
|
||||||
) {
|
List<String> line = new ArrayList<>();
|
||||||
Deque<Word> wordQ = new ArrayDeque<>(words);
|
for (Word word : words) {
|
||||||
List<List<String>> wrappings = boxes.stream()
|
if ((end + word.width) > width) {
|
||||||
.map(rectangle2D -> {
|
lines.add(String.join(" ", line));
|
||||||
double width = rectangle2D.getWidth();
|
line.clear();
|
||||||
double height = rectangle2D.getHeight();
|
end = 0;
|
||||||
List<String> lines = new ArrayList<>();
|
}
|
||||||
int bottom = 0;
|
line.add(word.word);
|
||||||
int end = 0;
|
end += word.width;
|
||||||
Deque<Word> lineQ = new ArrayDeque<>();
|
|
||||||
while (!wordQ.isEmpty()) {
|
|
||||||
Word word = wordQ.pop();
|
|
||||||
if ((bottom + word.height) > height) {
|
|
||||||
wordQ.add(word);
|
|
||||||
lineQ.forEach(wordQ::push);
|
|
||||||
return removeBlankLines(lines);
|
|
||||||
}
|
|
||||||
if (end == 0 && word.width > width) {
|
|
||||||
throw new WordTooLong(word.word);
|
|
||||||
}
|
|
||||||
if ((end + word.width) > width) {
|
|
||||||
lines.add(wordsAsString((Deque<Word>) lineQ));
|
|
||||||
lineQ.clear();
|
|
||||||
end = 0;
|
|
||||||
bottom += word.height;
|
|
||||||
}
|
|
||||||
lineQ.add(word);
|
|
||||||
end += word.width;
|
|
||||||
}
|
|
||||||
lines.add(wordsAsString(lineQ));
|
|
||||||
return removeBlankLines(lines);
|
|
||||||
}).collect(Collectors.toList());
|
|
||||||
if (wordQ.isEmpty()) {
|
|
||||||
return wrappings;
|
|
||||||
}
|
}
|
||||||
throw new NotEnoughSpace(wordQ.size());
|
lines.add(String.join(" ", line));
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> removeBlankLines(List<String> lines) {
|
|
||||||
return lines.stream()
|
return lines.stream()
|
||||||
.filter(l -> l.length() > 0)
|
.filter(l -> l.length() > 0)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String wordsAsString(Deque<Word> lineQ) {
|
|
||||||
return lineQ.stream()
|
|
||||||
.map(Word::getWord)
|
|
||||||
.collect(Collectors.joining(" "));
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Word> wordLengths(String[] words, Font font, Graphics2D graphics2D) {
|
private List<Word> wordLengths(String[] words, Font font, Graphics2D graphics2D) {
|
||||||
FontRenderContext fontRenderContext = graphics2D.getFontRenderContext();
|
FontRenderContext fontRenderContext = graphics2D.getFontRenderContext();
|
||||||
return Arrays.stream(words)
|
return Arrays.stream(words)
|
||||||
|
@ -97,16 +49,13 @@ class TextLineWrapImpl implements WordWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Word {
|
private static class Word {
|
||||||
@Getter
|
|
||||||
private final String word;
|
private final String word;
|
||||||
private final int width;
|
private final int width;
|
||||||
private final int height;
|
|
||||||
|
|
||||||
public Word(String word, Font font, FontRenderContext fontRenderContext) {
|
public Word(String word, Font font, FontRenderContext fontRenderContext) {
|
||||||
this.word = word;
|
this.word = word;
|
||||||
Rectangle2D stringBounds = font.getStringBounds(word + " ", fontRenderContext);
|
Rectangle2D stringBounds = font.getStringBounds(word + " ", fontRenderContext);
|
||||||
this.width = Double.valueOf(stringBounds.getWidth()).intValue();
|
this.width = Double.valueOf(stringBounds.getWidth()).intValue();
|
||||||
this.height = Double.valueOf(stringBounds.getHeight()).intValue();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
package net.kemitix.text.fit;
|
|
||||||
|
|
||||||
public class Tuple<A, B> {
|
|
||||||
|
|
||||||
private final A partA;
|
|
||||||
private final B partB;
|
|
||||||
|
|
||||||
private Tuple(final A partA, final B partB) {
|
|
||||||
this.partA = partA;
|
|
||||||
this.partB = partB;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <A, B> Tuple<A, B> of(final A a, final B b) {
|
|
||||||
return new Tuple<>(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public A get1() {
|
|
||||||
return partA;
|
|
||||||
}
|
|
||||||
|
|
||||||
public B get2() {
|
|
||||||
return partB;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
package net.kemitix.text.fit;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class WordTooLong extends RuntimeException {
|
|
||||||
private final String longWord;
|
|
||||||
}
|
|
|
@ -1,49 +1,13 @@
|
||||||
package net.kemitix.text.fit;
|
package net.kemitix.text.fit;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface WordWrapper {
|
public interface WordWrapper {
|
||||||
/**
|
|
||||||
* Wraps the text using the font in the graphics context to fit within the
|
|
||||||
* width.
|
|
||||||
*
|
|
||||||
* @param text the text to be line wrapped
|
|
||||||
* @param font the font to calculate character widths from
|
|
||||||
* @param graphics2D the context into which the font would be rendered
|
|
||||||
* @param width the maximum width of each line in pixels
|
|
||||||
* @return a list of the each line of text
|
|
||||||
* @throws NotEnoughSpace if there are more than {@link Integer#MAX_VALUE}
|
|
||||||
* lines - so not likely.
|
|
||||||
* @throws WordTooLong if there is a word that is too long to fit on a line
|
|
||||||
* by itself.
|
|
||||||
*/
|
|
||||||
List<String> wrap(
|
List<String> wrap(
|
||||||
String text,
|
String text,
|
||||||
Font font,
|
Font font,
|
||||||
Graphics2D graphics2D,
|
Graphics2D graphics2D,
|
||||||
int width
|
int width
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps the text using the font in the graphics context to fit within the
|
|
||||||
* boxes, filling them in order.
|
|
||||||
*
|
|
||||||
* @param text the text to be line wrapped
|
|
||||||
* @param font the font to calculate character widths from
|
|
||||||
* @param graphics2D the context into which the font would be rendered
|
|
||||||
* @param boxes the list of rectangles to fit each line within.
|
|
||||||
* @return a list of the each line of text
|
|
||||||
* @throws NotEnoughSpace if there are more lines than can be fitted in the
|
|
||||||
* boxes provided.
|
|
||||||
* @throws WordTooLong if there is a word that is too long to fit on a line
|
|
||||||
* by itself. Not likely as this would simply force smaller fonts.
|
|
||||||
*/
|
|
||||||
List<List<String>> wrap(
|
|
||||||
String text,
|
|
||||||
Font font,
|
|
||||||
Graphics2D graphics2D,
|
|
||||||
List<Rectangle2D> boxes
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package net.kemitix.text.fit;
|
||||||
|
|
||||||
import org.assertj.core.api.WithAssertions;
|
import org.assertj.core.api.WithAssertions;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
@ -12,10 +11,9 @@ import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
@ -24,161 +22,82 @@ public class BoxFitterTest
|
||||||
implements WithAssertions {
|
implements WithAssertions {
|
||||||
|
|
||||||
private final BoxFitter boxFitter = TextFit.fitter();
|
private final BoxFitter boxFitter = TextFit.fitter();
|
||||||
|
private final int imageSize = 300;
|
||||||
|
private final int fontSize = 20;
|
||||||
|
private final Graphics2D graphics2D = graphics(imageSize, imageSize);
|
||||||
private final Font font;
|
private final Font font;
|
||||||
private Function<Integer, Font> fontFactory;
|
private Function<Integer, Font> fontFactory;
|
||||||
private final int imageSize = 300;
|
|
||||||
private final Graphics2D graphics2D = graphics(imageSize, imageSize);
|
|
||||||
private Rectangle2D box = new Rectangle(imageSize, imageSize);
|
private Rectangle2D box = new Rectangle(imageSize, imageSize);
|
||||||
|
|
||||||
public BoxFitterTest() throws URISyntaxException, IOException, FontFormatException {
|
public BoxFitterTest() throws URISyntaxException, IOException, FontFormatException {
|
||||||
URL resource = this.getClass().getResource("alice/Alice-Regular.ttf");
|
URL resource = this.getClass().getResource("alice/Alice-Regular.ttf");
|
||||||
int initialFontSize = 20;
|
|
||||||
font = Font.createFont(Font.TRUETYPE_FONT, new File(resource.toURI()))
|
font = Font.createFont(Font.TRUETYPE_FONT, new File(resource.toURI()))
|
||||||
.deriveFont(Font.PLAIN, initialFontSize);
|
.deriveFont(Font.PLAIN, fontSize);
|
||||||
fontFactory = size -> font.deriveFont(Font.PLAIN, size);
|
fontFactory = size -> font.deriveFont(Font.PLAIN, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FitTests {
|
@Test
|
||||||
int fit(String text);
|
@DisplayName("Fit single words")
|
||||||
|
public void fitSingleWord() {
|
||||||
|
Map<String, Integer> wordMap = Map.of(
|
||||||
|
".", 263,
|
||||||
|
"a", 263,
|
||||||
|
"Word", 121,
|
||||||
|
"longer", 104,
|
||||||
|
"extralongword", 44
|
||||||
|
);
|
||||||
|
wordMap.forEach((word, expectedSize) ->
|
||||||
|
assertThat(invoke(word))
|
||||||
|
.as(word)
|
||||||
|
.isEqualTo(expectedSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Test
|
||||||
@DisplayName("Single Box API")
|
@DisplayName("Fit two words")
|
||||||
public class SingleBoxAPI implements FitTests {
|
public void fitTwoWords() {
|
||||||
|
Map<String, Integer> wordMap = Map.of(
|
||||||
@Override
|
". .", 263,
|
||||||
public int fit(String longText) {
|
"a a", 208,
|
||||||
return boxFitter.fit(longText, fontFactory, graphics2D, box);
|
"Another Word", 81,
|
||||||
}
|
longStringGenerator(1), 100,
|
||||||
|
longStringGenerator(2), 36,
|
||||||
@Test
|
longStringGenerator(3), 27,
|
||||||
@DisplayName("Fit single words")
|
longStringGenerator(4), 22,
|
||||||
public void fitSingleWord() {
|
longStringGenerator(5), 20,
|
||||||
Map<String, Integer> wordMap = Map.of(
|
longStringGenerator(100), 4,
|
||||||
".", 263,
|
longStringGenerator(196), 3
|
||||||
"a", 263,
|
);
|
||||||
"Word", 110,
|
wordMap.forEach((word, expectedSize) ->
|
||||||
"longer", 96,
|
assertThat(invoke(word))
|
||||||
"extralongword", 43
|
.as(word)
|
||||||
);
|
.isEqualTo(expectedSize));
|
||||||
wordMap.forEach((word, expectedSize) ->
|
|
||||||
assertThat(fit(word))
|
|
||||||
.as(word)
|
|
||||||
.isEqualTo(expectedSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Fit various lengths")
|
|
||||||
public void fitVariousLengths() {
|
|
||||||
Map<String, Integer> wordMap = Map.of(
|
|
||||||
". .", 263,
|
|
||||||
"a a", 208,
|
|
||||||
"Another Word", 76,
|
|
||||||
longStringGenerator(1), 93,
|
|
||||||
longStringGenerator(2), 36,
|
|
||||||
longStringGenerator(3), 27,
|
|
||||||
longStringGenerator(4), 22,
|
|
||||||
longStringGenerator(5), 20,
|
|
||||||
longStringGenerator(100), 4,
|
|
||||||
longStringGenerator(196), 3
|
|
||||||
);
|
|
||||||
wordMap.forEach((word, expectedSize) ->
|
|
||||||
assertThat(fit(word))
|
|
||||||
.as(word)
|
|
||||||
.isEqualTo(expectedSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Text too long to fit throws")
|
|
||||||
// too long to fit means it would need to be rendered at a font size of <2
|
|
||||||
public void tooLongThrows() {
|
|
||||||
String longText = longStringGenerator(197);
|
|
||||||
assertThatExceptionOfType(NotEnoughSpace.class)
|
|
||||||
.isThrownBy(() -> fit(longText));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Long text can be fitted down to a font size of 3")
|
|
||||||
public void veryLongFits() {
|
|
||||||
String longText = longStringGenerator(196);
|
|
||||||
assertThatCode(() -> fit(longText))
|
|
||||||
.doesNotThrowAnyException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Test
|
||||||
@DisplayName("List of Boxes API")
|
@DisplayName("Text too long to fit throws and exception")
|
||||||
public class BoxListAPI {
|
// too long to fit means it would need to be rendered at a font size of <2
|
||||||
|
public void tooLongThrows() {
|
||||||
|
String longText = longStringGenerator(197);
|
||||||
|
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||||
|
.isThrownBy(() -> invoke(longText));
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Test
|
||||||
@DisplayName("Single Box")
|
@DisplayName("Long text can be fitted down to a font size of 3")
|
||||||
// different API, but should have same behaviour as using single box API
|
public void veryLongFits() {
|
||||||
public class SingleBox implements FitTests {
|
String longText = longStringGenerator(196);
|
||||||
|
assertThatCode(() -> invoke(longText))
|
||||||
private final List<Rectangle2D> boxes = Collections.singletonList(box);
|
.doesNotThrowAnyException();
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public int fit(String longText) {
|
|
||||||
return boxFitter.fit(longText, fontFactory, graphics2D, boxes);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Fit various lengths")
|
|
||||||
public void fitVariousLengths() {
|
|
||||||
Map<String, Integer> wordMap = Map.of(
|
|
||||||
". .", 263,
|
|
||||||
"a a", 208,
|
|
||||||
"Another Word", 76,
|
|
||||||
longStringGenerator(1), 93,
|
|
||||||
longStringGenerator(2), 36,
|
|
||||||
longStringGenerator(3), 27,
|
|
||||||
longStringGenerator(4), 22,
|
|
||||||
longStringGenerator(5), 20,
|
|
||||||
longStringGenerator(100), 4,
|
|
||||||
longStringGenerator(196), 3
|
|
||||||
);
|
|
||||||
wordMap.forEach((word, expectedSize) ->
|
|
||||||
assertThat(fit(word))
|
|
||||||
.as(word)
|
|
||||||
.isEqualTo(expectedSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Excess text for one box - throws")
|
|
||||||
public void tooMuchForOneBox() {
|
|
||||||
assertThatExceptionOfType(NotEnoughSpace.class)
|
|
||||||
.isThrownBy(() ->
|
|
||||||
fit(longStringGenerator(197)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
@DisplayName("Two Boxes")
|
|
||||||
public class TwoBoxes implements FitTests{
|
|
||||||
|
|
||||||
private final List<Rectangle2D> boxes = Arrays.asList(box, box);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int fit(String longText) {
|
|
||||||
return boxFitter.fit(longText, fontFactory, graphics2D, boxes);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Excess text for one box - fits into two")
|
|
||||||
public void tooLongThrows() {
|
|
||||||
assertThatCode(() ->
|
|
||||||
fit(longStringGenerator(197)))
|
|
||||||
.doesNotThrowAnyException();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private int invoke(String longText) {
|
||||||
|
return boxFitter.fit(longText, fontFactory, graphics2D, box);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String longStringGenerator(int cycles) {
|
private String longStringGenerator(int cycles) {
|
||||||
String text = "This is a long piece of text that should result in an " +
|
String text = "This is a long piece of text that should result in an " +
|
||||||
"attempt to render it at a font size on less than 2.";
|
"attempt to render it at a font size on less than 2.";
|
||||||
return "cycles: " + cycles + IntStream.range(0, cycles)
|
return "cycles: " + cycles + IntStream.range(0, cycles)
|
||||||
.mapToObj(x -> "\n").collect(Collectors.joining(text));
|
.mapToObj(x -> "\n").collect(Collectors.joining(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
package net.kemitix.text.fit;
|
|
||||||
|
|
||||||
import org.assertj.core.api.WithAssertions;
|
|
||||||
import org.junit.jupiter.api.DisplayName;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
class StreamZipperTest implements WithAssertions {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void privateUtilityConstructor() throws NoSuchMethodException {
|
|
||||||
//given
|
|
||||||
final Constructor<StreamZipper> constructor = StreamZipper.class.getDeclaredConstructor();
|
|
||||||
constructor.setAccessible(true);
|
|
||||||
//then
|
|
||||||
assertThatCode(constructor::newInstance)
|
|
||||||
.hasCauseInstanceOf(UnsupportedOperationException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Pair two lists together")
|
|
||||||
void pairItems() {
|
|
||||||
//when
|
|
||||||
final List<String> strings = Arrays.asList("One", "Two", "Three");
|
|
||||||
final List<Integer> integers = Arrays.asList(3, 2, 1);
|
|
||||||
final List<Tuple<String, Integer>> zipped =
|
|
||||||
StreamZipper.zip(strings, integers, Tuple::of).collect(Collectors.toList());
|
|
||||||
//then
|
|
||||||
assertThat(zipped)
|
|
||||||
.extracting(Tuple::get1)
|
|
||||||
.containsExactlyElementsOf(strings.subList(0, zipped.size()));
|
|
||||||
assertThat(zipped)
|
|
||||||
.extracting(Tuple::get2)
|
|
||||||
.containsExactlyElementsOf(integers.subList(0, zipped.size()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +1,25 @@
|
||||||
package net.kemitix.text.fit;
|
package net.kemitix.text.fit;
|
||||||
|
|
||||||
import org.assertj.core.api.WithAssertions;
|
import org.assertj.core.api.WithAssertions;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
public class TextLineWrapTest
|
public class TextLineWrapTest
|
||||||
implements WithAssertions {
|
implements WithAssertions {
|
||||||
|
|
||||||
private static final String WORD = "word";
|
|
||||||
private static final String SPACE = " ";
|
|
||||||
|
|
||||||
private final WordWrapper textLineWrap = TextFit.wrapper();
|
private final WordWrapper textLineWrap = TextFit.wrapper();
|
||||||
private final int imageSize = 300;
|
private final int imageSize = 300;
|
||||||
private final int fontSize = 20;
|
private final int fontSize = 20;
|
||||||
private final Graphics2D graphics2D = graphics(imageSize, imageSize);
|
private final Graphics2D graphics2D = graphics(imageSize, imageSize);
|
||||||
private final Font font;
|
private final Font font;
|
||||||
private final int spaceWidth;
|
|
||||||
private final int wordWidth;
|
|
||||||
private final int wordsPerLine;
|
|
||||||
private final int wordHeight;
|
|
||||||
private final int linesPerBox;
|
|
||||||
|
|
||||||
public TextLineWrapTest()
|
public TextLineWrapTest()
|
||||||
throws FontFormatException,
|
throws FontFormatException,
|
||||||
|
@ -44,87 +28,46 @@ public class TextLineWrapTest
|
||||||
URL resource = this.getClass().getResource("alice/Alice-Regular.ttf");
|
URL resource = this.getClass().getResource("alice/Alice-Regular.ttf");
|
||||||
font = Font.createFont(Font.TRUETYPE_FONT, new File(resource.toURI()))
|
font = Font.createFont(Font.TRUETYPE_FONT, new File(resource.toURI()))
|
||||||
.deriveFont(Font.PLAIN, fontSize);
|
.deriveFont(Font.PLAIN, fontSize);
|
||||||
Rectangle2D spaceBounds = stringBounds(SPACE, font, graphics2D);
|
|
||||||
Rectangle2D wordBounds = stringBounds(WORD, font, graphics2D);
|
|
||||||
spaceWidth = (int) spaceBounds.getWidth();
|
|
||||||
wordWidth = (int) wordBounds.getWidth();
|
|
||||||
wordHeight = (int) wordBounds.getHeight();
|
|
||||||
wordsPerLine = imageSize / (wordWidth + spaceWidth);
|
|
||||||
linesPerBox = imageSize / wordHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Rectangle2D stringBounds(
|
private List<String> invoke(String in) {
|
||||||
String string,
|
return textLineWrap.wrap(in, font, graphics2D, imageSize);
|
||||||
Font font,
|
|
||||||
Graphics2D graphics2D
|
|
||||||
) {
|
|
||||||
return font.getStringBounds(string, graphics2D.getFontRenderContext());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Test
|
||||||
@DisplayName("Single box")
|
@DisplayName("Empty String give empty List")
|
||||||
public class SingleBox {
|
public void emptyStringEmptyList() {
|
||||||
|
assertThat(invoke("")).isEmpty();
|
||||||
private List<String> invoke(String in) {
|
|
||||||
return textLineWrap.wrap(in, font, graphics2D, imageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Empty String give empty List")
|
|
||||||
public void emptyStringEmptyList() {
|
|
||||||
assertThat(invoke("")).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Short string fits on one line")
|
|
||||||
public void shortStringOnOneLine() {
|
|
||||||
assertThat(invoke("x")).containsExactly("x");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Fits max 'words' per line on one line")
|
|
||||||
public void fitMaxWordsOneLine() {
|
|
||||||
String input = words(wordsPerLine, WORD);
|
|
||||||
assertThat(invoke(input)).containsExactly(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Wraps just over max 'words' per line onto two lines")
|
|
||||||
public void wrapOneWordTooManyOntoTwoLines() {
|
|
||||||
assertThat(invoke(words(wordsPerLine + 1, WORD)))
|
|
||||||
.containsExactly(
|
|
||||||
words(wordsPerLine, WORD),
|
|
||||||
WORD);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Wraps onto three lines")
|
|
||||||
public void longerStringOnThreeLines() {
|
|
||||||
String oneLinesWorthOfWords = words(wordsPerLine, WORD);
|
|
||||||
assertThat(invoke(words(wordsPerLine + wordsPerLine + 1, WORD)))
|
|
||||||
.containsExactly(
|
|
||||||
oneLinesWorthOfWords,
|
|
||||||
oneLinesWorthOfWords,
|
|
||||||
WORD);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("A word that can't fit on a line by itself throws")
|
|
||||||
public void overLongWordThrows() {
|
|
||||||
String longWord = words(wordsPerLine * 2, WORD)
|
|
||||||
.replace(" ", "");
|
|
||||||
assertThatExceptionOfType(WordTooLong.class)
|
|
||||||
.isThrownBy(() -> invoke(longWord))
|
|
||||||
.satisfies(error ->
|
|
||||||
assertThat(error.getLongWord())
|
|
||||||
.isEqualTo(longWord));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String words(int number, String word) {
|
@Test
|
||||||
return IntStream.range(0, number)
|
@DisplayName("Short string fits on one line")
|
||||||
.mapToObj(i -> word + SPACE)
|
public void shortStringOnOneLine() {
|
||||||
.collect(Collectors.joining()).trim();
|
assertThat(invoke("x")).containsExactly("x");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Longer string fits on two lines")
|
||||||
|
public void longerStringOnTwoLines() {
|
||||||
|
assertThat(invoke(
|
||||||
|
"xxxxxxxxxxx xxxxxxxxxxxx " +
|
||||||
|
"xxxxxxxxxxx xxxxxxxxxxxx"))
|
||||||
|
.containsExactly(
|
||||||
|
"xxxxxxxxxxx xxxxxxxxxxxx",
|
||||||
|
"xxxxxxxxxxx xxxxxxxxxxxx");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Longer string fits on three lines")
|
||||||
|
public void longerStringOnThreeLines() {
|
||||||
|
assertThat(invoke(
|
||||||
|
"xxxxxxxxxxx xxxxxxxxxxxx " +
|
||||||
|
"xxxxxxxxxxx xxxxxxxxxxxx " +
|
||||||
|
"xxxxxxxxxxx xxxxxxxxxxxx"))
|
||||||
|
.containsExactly(
|
||||||
|
"xxxxxxxxxxx xxxxxxxxxxxx",
|
||||||
|
"xxxxxxxxxxx xxxxxxxxxxxx",
|
||||||
|
"xxxxxxxxxxx xxxxxxxxxxxx");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Graphics2D graphics(int width, int height) {
|
private Graphics2D graphics(int width, int height) {
|
||||||
|
@ -135,181 +78,4 @@ public class TextLineWrapTest
|
||||||
private BufferedImage image(int width, int height) {
|
private BufferedImage image(int width, int height) {
|
||||||
return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
|
||||||
@DisplayName("Overflowing Boxes")
|
|
||||||
public class OverflowingBoxes {
|
|
||||||
|
|
||||||
private List<Rectangle2D> boxes = new ArrayList<>();
|
|
||||||
|
|
||||||
private List<List<String>> wrap(String in) {
|
|
||||||
return textLineWrap.wrap(in, font, graphics2D, boxes);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
@DisplayName("Single box")
|
|
||||||
public class SingleBox {
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void setUp() {
|
|
||||||
boxes.add(new Rectangle(imageSize, imageSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Empty String give empty List")
|
|
||||||
public void emptyStringEmptyList() {
|
|
||||||
assertThat(wrap("")).containsExactly(Collections.emptyList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Short string fits on one line")
|
|
||||||
public void shortStringOnOneLine() {
|
|
||||||
assertThat(wrap("x"))
|
|
||||||
.containsExactly(Collections.singletonList("x"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Fits max 'words' per line on one line")
|
|
||||||
public void fitMaxWordsOneLine() {
|
|
||||||
String input = words(wordsPerLine, WORD);
|
|
||||||
assertThat(wrap(input))
|
|
||||||
.containsExactly(Collections.singletonList(input));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Wraps just over max 'words' per line onto two lines")
|
|
||||||
public void wrapOneWordTooManyOntoTwoLines() {
|
|
||||||
String input = words(wordsPerLine + 1, WORD);
|
|
||||||
assertThat(wrap(input)).containsExactly(Arrays.asList(
|
|
||||||
words(wordsPerLine, WORD),
|
|
||||||
WORD));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Wraps onto three lines")
|
|
||||||
public void longerStringOnThreeLines() {
|
|
||||||
String input = words(wordsPerLine + wordsPerLine + 1, WORD);
|
|
||||||
String oneLinesWorthOfWords = words(wordsPerLine, WORD);
|
|
||||||
assertThat(wrap(input)).containsExactly(Arrays.asList(
|
|
||||||
oneLinesWorthOfWords,
|
|
||||||
oneLinesWorthOfWords,
|
|
||||||
WORD));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Text fills the box")
|
|
||||||
public void textFillsBox() {
|
|
||||||
String lineOfWords = words(wordsPerLine, WORD);
|
|
||||||
String words = words(linesPerBox, lineOfWords);
|
|
||||||
assertThat(wrap(words).get(0))
|
|
||||||
.hasSize(linesPerBox)
|
|
||||||
.allSatisfy(line ->
|
|
||||||
assertThat(line).isEqualTo(lineOfWords));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Text overflows the box")
|
|
||||||
public void tooManyLinesForBox() {
|
|
||||||
String lineOfWords = words(wordsPerLine, WORD);
|
|
||||||
String input = words(linesPerBox + 1, lineOfWords);
|
|
||||||
assertThatExceptionOfType(NotEnoughSpace.class)
|
|
||||||
.isThrownBy(() -> wrap(input))
|
|
||||||
.satisfies(error ->
|
|
||||||
assertThat(error.getExcessWordCount())
|
|
||||||
.isEqualTo(wordsPerLine));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
@DisplayName("Two boxes of equal width")
|
|
||||||
public class TwoEqualWidthBoxes {
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void setUp() {
|
|
||||||
Rectangle box = new Rectangle(imageSize, imageSize);
|
|
||||||
boxes.add(box);
|
|
||||||
boxes.add(box);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that even with two boxes the shorted strings are unaffected
|
|
||||||
@Test
|
|
||||||
@DisplayName("Empty String give empty List")
|
|
||||||
public void emptyStringEmptyList() {
|
|
||||||
assertThat(wrap("")).containsExactly(
|
|
||||||
Collections.emptyList(),
|
|
||||||
Collections.emptyList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Short string fits on one line")
|
|
||||||
public void shortStringOnOneLine() {
|
|
||||||
assertThat(wrap("x"))
|
|
||||||
.containsExactly(
|
|
||||||
Collections.singletonList("x"),
|
|
||||||
Collections.emptyList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Fits max 'words' per line on one line")
|
|
||||||
public void fitMaxWordsOneLine() {
|
|
||||||
String input = words(wordsPerLine, WORD);
|
|
||||||
assertThat(wrap(input))
|
|
||||||
.containsExactly(
|
|
||||||
Collections.singletonList(input),
|
|
||||||
Collections.emptyList() );
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Wraps just over max 'words' per line onto two lines")
|
|
||||||
public void wrapOneWordTooManyOntoTwoLines() {
|
|
||||||
String input = words(wordsPerLine + 1, WORD);
|
|
||||||
assertThat(wrap(input)).containsExactly(Arrays.asList(
|
|
||||||
words(wordsPerLine, WORD),
|
|
||||||
WORD),
|
|
||||||
Collections.emptyList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Wraps onto three lines")
|
|
||||||
public void longerStringOnThreeLines() {
|
|
||||||
String input = words(wordsPerLine + wordsPerLine + 1, WORD);
|
|
||||||
String oneLinesWorthOfWords = words(wordsPerLine, WORD);
|
|
||||||
assertThat(wrap(input)).containsExactly(Arrays.asList(
|
|
||||||
oneLinesWorthOfWords,
|
|
||||||
oneLinesWorthOfWords,
|
|
||||||
WORD),
|
|
||||||
Collections.emptyList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Fit max lines into the first box")
|
|
||||||
public void fitMaxLinesInFirstBox() {
|
|
||||||
String oneLinesWorthOfWords = words(wordsPerLine, WORD);
|
|
||||||
String input = words(linesPerBox, oneLinesWorthOfWords);
|
|
||||||
List<List<String>> result = wrap(input);
|
|
||||||
List<String> linesBoxOne = result.get(0);
|
|
||||||
assertThat(linesBoxOne)
|
|
||||||
.hasSize(linesPerBox)
|
|
||||||
.allSatisfy(line ->
|
|
||||||
assertThat(line)
|
|
||||||
.isEqualTo(oneLinesWorthOfWords));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Overflow just over max lines into the second box")
|
|
||||||
public void overflowMaxPlusOneLinesIntoSecondBox() {
|
|
||||||
String oneLinesWorthOfWords = words(wordsPerLine, WORD);
|
|
||||||
String input = words(linesPerBox + 1, oneLinesWorthOfWords);
|
|
||||||
List<List<String>> result = wrap(input);
|
|
||||||
List<String> linesBoxOne = result.get(0);
|
|
||||||
assertThat(linesBoxOne)
|
|
||||||
.hasSize(linesPerBox)
|
|
||||||
.allSatisfy(line ->
|
|
||||||
assertThat(line)
|
|
||||||
.isEqualTo(oneLinesWorthOfWords));
|
|
||||||
assertThat(result.get(1))
|
|
||||||
.containsExactly(oneLinesWorthOfWords);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue